diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b62fa9d4880..e1809bc7acb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,36 +45,35 @@ jobs: # Test without passport pytest -v --cov -k test_no_passport.py status=$? - + # test without pytz pytest -v --cov --cov-append -k test_datetime.py status=$(( $? > status ? $? : status)) pytest -v --cov --cov-append -k test_defaults.py status=$(( $? > status ? $? : status)) - + # test without pytz & jobqueue pytest -v --cov --cov-append -k test_jobqueue.py pytest -v --cov --cov-append -k test_applicationbuilder.py status=$(( $? > status ? $? : status)) - + # Test without ratelimiter pytest -v --cov --cov-append -k test_ratelimiter.py status=$(( $? > status ? $? : status)) - + # Test without webhooks pytest -v --cov --cov-append -k test_updater.py status=$(( $? > status ? $? : status)) - + # Test without callback-data pytest -v --cov --cov-append -k test_callbackdatacache.py status=$(( $? > status ? $? : status)) - + # Test without socks pytest -v --cov --cov-append -k test_request.py status=$(( $? > status ? $? : status)) - + # Test the rest - export TEST_WITH_OPT_DEPS='true' pip install -r requirements-opts.txt # `-n auto --dist loadfile` uses pytest-xdist to run each test file on a different CPU diff --git a/README.rst b/README.rst index 021e267e04a..542c80ae450 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.3-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions diff --git a/README_RAW.rst b/README_RAW.rst index 69d9642eb66..e71a2095a9c 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.3-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index df699e796aa..aff45cc164f 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -256,6 +256,35 @@
+.. raw:: html + +
+ Forum topic management + +.. list-table:: + :align: left + :widths: 1 4 + + * - :meth:`~telegram.Bot.close_forum_topic` + - Used for closing a forum topic + * - :meth:`~telegram.Bot.create_forum_topic` + - Used to create a topic + * - :meth:`~telegram.Bot.delete_forum_topic` + - Used for deleting a forum topic + * - :meth:`~telegram.Bot.edit_forum_topic` + - Used to edit a topic + * - :meth:`~telegram.Bot.reopen_forum_topic` + - Used to reopen a topic + * - :meth:`~telegram.Bot.get_forum_topic_icon_stickers` + - Used to get custom emojis to use as topic icons + * - :meth:`~telegram.Bot.unpin_all_forum_topic_messages` + - Used to unpin all messages in a forum topic + +.. raw:: html + +
+
+ .. raw:: html
diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 4f2a4ec1a97..41fd5e6385a 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -35,6 +35,10 @@ Available Types telegram.document telegram.file telegram.forcereply + telegram.forumtopic + telegram.forumtopicclosed + telegram.forumtopiccreated + telegram.forumtopicreopened telegram.inlinekeyboardbutton telegram.inlinekeyboardmarkup telegram.inputfile diff --git a/docs/source/telegram.forumtopic.rst b/docs/source/telegram.forumtopic.rst new file mode 100644 index 00000000000..a2dd6389c77 --- /dev/null +++ b/docs/source/telegram.forumtopic.rst @@ -0,0 +1,6 @@ +telegram.ForumTopic +=================== + +.. autoclass:: telegram.ForumTopic + :members: + :show-inheritance: diff --git a/docs/source/telegram.forumtopicclosed.rst b/docs/source/telegram.forumtopicclosed.rst new file mode 100644 index 00000000000..198a6737255 --- /dev/null +++ b/docs/source/telegram.forumtopicclosed.rst @@ -0,0 +1,6 @@ +telegram.ForumTopicClosed +========================= + +.. autoclass:: telegram.ForumTopicClosed + :members: + :show-inheritance: diff --git a/docs/source/telegram.forumtopiccreated.rst b/docs/source/telegram.forumtopiccreated.rst new file mode 100644 index 00000000000..f1da6f81d37 --- /dev/null +++ b/docs/source/telegram.forumtopiccreated.rst @@ -0,0 +1,6 @@ +telegram.ForumTopicCreated +========================== + +.. autoclass:: telegram.ForumTopicCreated + :members: + :show-inheritance: diff --git a/docs/source/telegram.forumtopicreopened.rst b/docs/source/telegram.forumtopicreopened.rst new file mode 100644 index 00000000000..b81c395d254 --- /dev/null +++ b/docs/source/telegram.forumtopicreopened.rst @@ -0,0 +1,6 @@ +telegram.ForumTopicReopened +=========================== + +.. autoclass:: telegram.ForumTopicReopened + :members: + :show-inheritance: diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index 75c1c90cbe7..c021020a799 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -26,6 +26,10 @@ .. |chat_id_group| replace:: Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``). +.. |message_thread_id| replace:: Unique identifier for the target message thread of the forum topic. + +.. |message_thread_id_arg| replace:: Unique identifier for the target message thread (topic) of the forum; for forum supergroups only. + .. |parse_mode| replace:: Mode for parsing entities. See :class:`telegram.constants.ParseMode` and `formatting options `__ for more details. .. |allow_sending_without_reply| replace:: Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. diff --git a/telegram/__init__.py b/telegram/__init__.py index 11b5e9b720f..c0230f076c8 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -67,6 +67,10 @@ "File", "FileCredentials", "ForceReply", + "ForumTopic", + "ForumTopicClosed", + "ForumTopicCreated", + "ForumTopicReopened", "Game", "GameHighScore", "helpers", @@ -230,6 +234,7 @@ from ._files.videonote import VideoNote from ._files.voice import Voice from ._forcereply import ForceReply +from ._forumtopic import ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicReopened from ._games.callbackgame import CallbackGame from ._games.game import Game from ._games.gamehighscore import GameHighScore diff --git a/telegram/_bot.py b/telegram/_bot.py index d7e83362f51..5ff220ee99a 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -74,6 +74,7 @@ from telegram._files.video import Video from telegram._files.videonote import VideoNote from telegram._files.voice import Voice +from telegram._forumtopic import ForumTopic from telegram._games.gamehighscore import GameHighScore from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._menubutton import MenuButton @@ -440,6 +441,7 @@ async def _send_message( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -459,6 +461,9 @@ async def _send_message( if reply_markup is not None: data["reply_markup"] = reply_markup + if message_thread_id is not None: + data["message_thread_id"] = message_thread_id + result = await self._post( endpoint, data, @@ -660,6 +665,7 @@ async def send_message( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -693,6 +699,9 @@ async def send_message( :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 Returns: :class:`telegram.Message`: On success, the sent message is returned. @@ -719,6 +728,7 @@ async def send_message( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -743,6 +753,7 @@ async def delete_message( limitations: - A message can only be deleted if it was sent less than 48 hours ago. + - Service messages about a supergroup, channel, or forum topic creation can't be deleted. - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago. - Bots can delete outgoing messages in private chats, groups, and supergroups. @@ -787,6 +798,7 @@ async def forward_message( message_id: int, disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -818,6 +830,9 @@ async def forward_message( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 Returns: :class:`telegram.Message`: On success, the sent Message is returned. @@ -838,6 +853,7 @@ async def forward_message( data, disable_notification=disable_notification, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -858,6 +874,7 @@ async def send_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -893,6 +910,9 @@ async def send_photo( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -934,6 +954,7 @@ async def send_photo( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -958,6 +979,7 @@ async def send_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1004,6 +1026,10 @@ async def send_audio( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -1062,6 +1088,7 @@ async def send_audio( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1084,6 +1111,7 @@ async def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1129,6 +1157,10 @@ async def send_document( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -1181,6 +1213,7 @@ async def send_document( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1198,6 +1231,7 @@ async def send_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -1228,6 +1262,10 @@ async def send_sticker( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -1251,6 +1289,7 @@ async def send_sticker( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1276,6 +1315,7 @@ async def send_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1327,6 +1367,10 @@ async def send_video( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -1386,6 +1430,7 @@ async def send_video( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1406,6 +1451,7 @@ async def send_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1450,6 +1496,10 @@ async def send_video_note( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -1500,6 +1550,7 @@ async def send_video_note( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1524,6 +1575,7 @@ async def send_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1579,6 +1631,10 @@ async def send_animation( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -1627,6 +1683,7 @@ async def send_animation( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1648,6 +1705,7 @@ async def send_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1693,6 +1751,10 @@ async def send_voice( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -1736,6 +1798,7 @@ async def send_voice( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1754,6 +1817,7 @@ async def send_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -1784,6 +1848,10 @@ async def send_media_group( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1847,6 +1915,9 @@ async def send_media_group( if reply_to_message_id: data["reply_to_message_id"] = reply_to_message_id + if message_thread_id: + data["message_thread_id"] = message_thread_id + result = await self._post( "sendMediaGroup", data, @@ -1874,6 +1945,7 @@ async def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: Location = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1910,6 +1982,10 @@ async def send_location( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -1960,6 +2036,7 @@ async def send_location( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2137,6 +2214,7 @@ async def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: Venue = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2176,6 +2254,9 @@ async def send_venue( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2242,6 +2323,7 @@ async def send_venue( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2262,6 +2344,7 @@ async def send_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: Contact = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2291,6 +2374,9 @@ async def send_contact( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2346,6 +2432,7 @@ async def send_contact( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2363,6 +2450,7 @@ async def send_game( reply_markup: InlineKeyboardMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2383,6 +2471,10 @@ async def send_game( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new @@ -2406,6 +2498,7 @@ async def send_game( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -4057,6 +4150,7 @@ async def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -4147,6 +4241,10 @@ async def send_invoice( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an @@ -4208,6 +4306,7 @@ async def send_invoice( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -4460,6 +4559,7 @@ async def promote_chat_member( is_anonymous: bool = None, can_manage_chat: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -4513,6 +4613,10 @@ async def promote_chat_member( add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him). + can_manage_topics (:obj:`bool`, optional): Pass :obj:`True`, if the user is + allowed to create, rename, close, and reopen forum topics; supergroups only. + + .. versionadded:: 20.0 Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -4545,6 +4649,8 @@ async def promote_chat_member( data["can_manage_chat"] = can_manage_chat if can_manage_video_chats is not None: data["can_manage_video_chats"] = can_manage_video_chats + if can_manage_topics is not None: + data["can_manage_topics"] = can_manage_topics result = await self._post( "promoteChatMember", @@ -5111,7 +5217,9 @@ async def set_chat_title( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - title (:obj:`str`): New chat title, 1-255 characters. + title (:obj:`str`): New chat title, + :tg-const:`telegram.constants.ChatLimit.MIN_CHAT_TITLE_LENGTH`- + :tg-const:`telegram.constants.ChatLimit.MAX_CHAT_TITLE_LENGTH` characters. Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -5856,6 +5964,7 @@ async def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -5908,6 +6017,10 @@ async def send_poll( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -5956,6 +6069,7 @@ async def send_poll( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -6020,6 +6134,7 @@ async def send_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -6057,6 +6172,9 @@ async def send_dice( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 Returns: :class:`telegram.Message`: On success, the sent Message is returned. @@ -6077,6 +6195,7 @@ async def send_dice( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -6433,6 +6552,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -6464,6 +6584,10 @@ async def copy_message( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ @@ -6495,6 +6619,8 @@ async def copy_message( data["reply_to_message_id"] = reply_to_message_id if reply_markup: data["reply_markup"] = reply_markup + if message_thread_id: + data["message_thread_id"] = message_thread_id result = await self._post( "copyMessage", @@ -6523,7 +6649,7 @@ async def set_chat_menu_button( button. .. seealso:: :meth:`get_chat_menu_button`, :meth:`telegram.Chat.set_menu_button`, - :meth:`telegram.Chat.get_menu_button`, meth:`telegram.User.set_menu_button`, + :meth:`telegram.Chat.get_menu_button`, :meth:`telegram.User.set_menu_button`, :meth:`telegram.User.get_menu_button` .. versionadded:: 20.0 @@ -6734,6 +6860,349 @@ async def create_invoice_link( api_kwargs=api_kwargs, ) + @_log + async def get_forum_topic_icon_stickers( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> List[Sticker]: + """Use this method to get custom emoji stickers, which can be used as a forum topic + icon by any user. Requires no parameters. + + .. versionadded:: 20.0 + + Returns: + List[:class:`telegram.Sticker`] + + Raises: + :class:`telegram.error.TelegramError` + + """ + result = await self._post( + "getForumTopicIconStickers", + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return Sticker.de_list(result, self) # type: ignore[return-value, arg-type] + + @_log + async def create_forum_topic( + self, + chat_id: Union[str, int], + name: str, + icon_color: int = None, + icon_custom_emoji_id: str = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> 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 + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. seealso:: :meth:`telegram.Chat.create_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + name (:obj:`str`): New topic name, + :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. + icon_color (:obj:`int`, optional): Color of the topic icon in RGB format. Currently, + must be one of :attr:`telegram.constants.ForumIconColor.BLUE`, + :attr:`telegram.constants.ForumIconColor.YELLOW`, + :attr:`telegram.constants.ForumIconColor.PURPLE`, + :attr:`telegram.constants.ForumIconColor.GREEN`, + :attr:`telegram.constants.ForumIconColor.PINK`, or + :attr:`telegram.constants.ForumIconColor.RED`. + icon_custom_emoji_id (:obj:`str`, optional): New unique identifier of the custom emoji + shown as the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` + to get all allowed custom emoji identifiers. + + Returns: + :class:`telegram.ForumTopic` + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "name": name, + "icon_color": icon_color, + "icon_custom_emoji_id": icon_custom_emoji_id, + } + result = await self._post( + "createForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return ForumTopic.de_json(result, self) # type: ignore[return-value, arg-type] + + @_log + async def edit_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + name: str, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, + unless it is the creator of the topic. + + .. seealso:: :meth:`telegram.Message.edit_forum_topic`, + :meth:`telegram.Chat.edit_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + name (:obj:`str`): New topic name, + :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. + icon_custom_emoji_id (:obj:`str`): New unique identifier of the custom emoji shown as + the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` to get all + allowed custom emoji identifiers. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + "name": name, + "icon_custom_emoji_id": icon_custom_emoji_id, + } + return await self._post( # type: ignore[return-value] + "editForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def close_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to close an open topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, + unless it is the creator of the topic. + + .. seealso:: :meth:`telegram.Message.close_forum_topic`, + :meth:`telegram.Chat.close_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( # type: ignore[return-value] + "closeForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def reopen_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to reopen a closed topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, + unless it is the creator of the topic. + + .. seealso:: :meth:`telegram.Message.reopen_forum_topic`, + :meth:`telegram.Chat.reopen_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( # type: ignore[return-value] + "reopenForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def delete_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to delete a forum topic along with all its messages in a forum supergroup + chat. The bot must be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_delete_messages` administrator rights. + + .. seealso:: :meth:`telegram.Message.delete_forum_topic`, + :meth:`telegram.Chat.delete_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( # type: ignore[return-value] + "deleteForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def unpin_all_forum_topic_messages( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to clear the list of pinned messages in a forum topic. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` administrator rights + in the supergroup. + + .. seealso:: :meth:`telegram.Message.unpin_all_forum_topic_messages`, + :meth:`telegram.Chat.unpin_all_forum_topic_messages`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( # type: ignore[return-value] + "unpinAllForumTopicMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # skipcq: PYL-W0613 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -6926,3 +7395,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 73674e0d491..2a6fd1ba682 100644 --- a/telegram/_callbackquery.py +++ b/telegram/_callbackquery.py @@ -724,6 +724,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -761,6 +762,7 @@ async def copy_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) MAX_ANSWER_TEXT_LENGTH: ClassVar[ diff --git a/telegram/_chat.py b/telegram/_chat.py index 296a32cc4da..3acf2d9e34c 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -148,6 +148,22 @@ class Chat(TelegramObject): in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 + is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum + (has topics_ enabled). + + .. versionadded:: 20.0 + active_usernames (List[:obj:`str`], optional): If set, the list of all `active chat + usernames `_; for private chats, supergroups and channels. Returned + only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + emoji_status_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of emoji + status of the other party in a private chat. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + Attributes: id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. @@ -212,8 +228,24 @@ class Chat(TelegramObject): privacy settings of the other party restrict sending voice and video note messages in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 + is_forum (:obj:`bool`): Optional. :obj:`True`, if the supergroup chat is a forum + (has topics_ enabled). + + .. versionadded:: 20.0 + active_usernames (List[:obj:`str`]): Optional. If set, the list of all `active chat + usernames `_; for private chats, supergroups and channels. Returned + only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + emoji_status_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji + status of the other party in a private chat. Returned only in + :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 + .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups """ __slots__ = ( @@ -240,6 +272,9 @@ class Chat(TelegramObject): "join_to_send_messages", "join_by_request", "has_restricted_voice_and_video_messages", + "is_forum", + "active_usernames", + "emoji_status_custom_emoji_id", ) SENDER: ClassVar[str] = constants.ChatType.SENDER @@ -281,6 +316,9 @@ def __init__( join_to_send_messages: bool = None, join_by_request: bool = None, has_restricted_voice_and_video_messages: bool = None, + is_forum: bool = None, + active_usernames: List[str] = None, + emoji_status_custom_emoji_id: str = None, *, api_kwargs: JSONDict = None, ): @@ -312,6 +350,9 @@ def __init__( self.join_to_send_messages = join_to_send_messages self.join_by_request = join_by_request self.has_restricted_voice_and_video_messages = has_restricted_voice_and_video_messages + self.is_forum = is_forum + self.active_usernames = active_usernames + self.emoji_status_custom_emoji_id = emoji_status_custom_emoji_id self._id_attrs = (self.id,) @@ -802,6 +843,7 @@ async def promote_member( is_anonymous: bool = None, can_manage_chat: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -845,6 +887,7 @@ async def promote_member( is_anonymous=is_anonymous, can_manage_chat=can_manage_chat, can_manage_video_chats=can_manage_video_chats, + can_manage_topics=can_manage_topics, ) async def restrict_member( @@ -1192,6 +1235,7 @@ async def send_message( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1220,6 +1264,7 @@ async def send_message( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1236,6 +1281,7 @@ async def send_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -1268,6 +1314,7 @@ async def send_media_group( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, @@ -1317,6 +1364,7 @@ async def send_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1347,6 +1395,7 @@ async def send_photo( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1365,6 +1414,7 @@ async def send_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: "Contact" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1400,6 +1450,7 @@ async def send_contact( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_audio( @@ -1417,6 +1468,7 @@ async def send_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1451,6 +1503,7 @@ async def send_audio( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1471,6 +1524,7 @@ async def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1508,6 +1562,7 @@ async def send_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_dice( @@ -1518,6 +1573,7 @@ async def send_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1548,6 +1604,7 @@ async def send_dice( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_game( @@ -1558,6 +1615,7 @@ async def send_game( reply_markup: "InlineKeyboardMarkup" = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1588,6 +1646,7 @@ async def send_game( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_invoice( @@ -1618,6 +1677,7 @@ async def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1678,6 +1738,7 @@ async def send_invoice( max_tip_amount=max_tip_amount, suggested_tip_amounts=suggested_tip_amounts, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_location( @@ -1693,6 +1754,7 @@ async def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: "Location" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1730,6 +1792,7 @@ async def send_location( proximity_alert_radius=proximity_alert_radius, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_animation( @@ -1747,6 +1810,7 @@ async def send_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1786,6 +1850,7 @@ async def send_animation( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_sticker( @@ -1796,6 +1861,7 @@ async def send_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -1826,6 +1892,7 @@ async def send_sticker( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_venue( @@ -1843,6 +1910,7 @@ async def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: "Venue" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1882,6 +1950,7 @@ async def send_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_video( @@ -1900,6 +1969,7 @@ async def send_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1940,6 +2010,7 @@ async def send_video( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_video_note( @@ -1953,6 +2024,7 @@ async def send_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1988,6 +2060,7 @@ async def send_video_note( allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_voice( @@ -2002,6 +2075,7 @@ async def send_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2038,6 +2112,7 @@ async def send_voice( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_poll( @@ -2059,6 +2134,7 @@ async def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2100,6 +2176,7 @@ async def send_poll( allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_copy( @@ -2114,6 +2191,7 @@ async def send_copy( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2148,6 +2226,7 @@ async def send_copy( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def copy_message( @@ -2162,6 +2241,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2196,6 +2276,7 @@ async def copy_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def forward_from( @@ -2204,6 +2285,7 @@ async def forward_from( message_id: int, disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2236,6 +2318,7 @@ async def forward_from( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def forward_to( @@ -2244,6 +2327,7 @@ async def forward_to( message_id: int, disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2276,6 +2360,7 @@ async def forward_to( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def export_invite_link( @@ -2532,6 +2617,207 @@ async def set_menu_button( api_kwargs=api_kwargs, ) + async def create_forum_topic( + self, + name: str, + icon_color: int = None, + icon_custom_emoji_id: str = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.create_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.create_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().create_forum_topic( + chat_id=self.id, + name=name, + icon_color=icon_color, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_forum_topic( + self, + message_thread_id: int, + name: str, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.edit_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().edit_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + name=name, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_forum_topic( + self, + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.close_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.close_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().close_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_forum_topic( + self, + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.reopen_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.reopen_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().reopen_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_forum_topic( + self, + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.delete_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_forum_topic_messages( + self, + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.unpin_all_forum_topic_messages(chat_id=update.effective_chat.id, + *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_forum_topic_messages`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().unpin_all_forum_topic_messages( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def get_menu_button( self, *, diff --git a/telegram/_chatadministratorrights.py b/telegram/_chatadministratorrights.py index 185782373ca..32175cba586 100644 --- a/telegram/_chatadministratorrights.py +++ b/telegram/_chatadministratorrights.py @@ -29,7 +29,12 @@ class ChatAdministratorRights(TelegramObject): considered equal, if their :attr:`is_anonymous`, :attr:`can_manage_chat`, :attr:`can_delete_messages`, :attr:`can_manage_video_chats`, :attr:`can_restrict_members`, :attr:`can_promote_members`, :attr:`can_change_info`, :attr:`can_invite_users`, - :attr:`can_post_messages`, :attr:`can_edit_messages`, :attr:`can_pin_messages` are equal. + :attr:`can_post_messages`, :attr:`can_edit_messages`, :attr:`can_pin_messages`, + :attr:`can_manage_topics` are equal. + + .. versionchanged:: 20.0 + :attr:`can_manage_topics` is considered as well when comparing objects of + this type in terms of equality. .. seealso: :meth:`Bot.set_my_default_administrator_rights`, :meth:`Bot.get_my_default_administrator_rights` @@ -62,6 +67,10 @@ class ChatAdministratorRights(TelegramObject): messages of other users. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. + can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only. + + .. versionadded:: 20.0 Attributes: is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. @@ -89,6 +98,10 @@ class ChatAdministratorRights(TelegramObject): messages of other users. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. + can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only. + + .. versionadded:: 20.0 """ __slots__ = ( @@ -103,6 +116,7 @@ class ChatAdministratorRights(TelegramObject): "can_post_messages", "can_edit_messages", "can_pin_messages", + "can_manage_topics", ) def __init__( @@ -118,6 +132,7 @@ def __init__( can_post_messages: bool = None, can_edit_messages: bool = None, can_pin_messages: bool = None, + can_manage_topics: bool = None, *, api_kwargs: JSONDict = None, ) -> None: @@ -135,6 +150,7 @@ def __init__( self.can_post_messages = can_post_messages self.can_edit_messages = can_edit_messages self.can_pin_messages = can_pin_messages + self.can_manage_topics = can_manage_topics self._id_attrs = ( self.is_anonymous, @@ -148,6 +164,7 @@ def __init__( self.can_post_messages, self.can_edit_messages, self.can_pin_messages, + self.can_manage_topics, ) @classmethod @@ -159,7 +176,7 @@ def all_rights(cls) -> "ChatAdministratorRights": .. versionadded:: 20.0 """ - return cls(True, True, True, True, True, True, True, True, True, True, True) + return cls(True, True, True, True, True, True, True, True, True, True, True, True) @classmethod def no_rights(cls) -> "ChatAdministratorRights": @@ -169,4 +186,6 @@ def no_rights(cls) -> "ChatAdministratorRights": .. versionadded:: 20.0 """ - return cls(False, False, False, False, False, False, False, False, False, False, False) + return cls( + False, False, False, False, False, False, False, False, False, False, False, False + ) diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index 363009af607..a1db9db3666 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -173,6 +173,8 @@ class ChatMemberAdministrator(ChatMember): * Argument and attribute ``can_manage_voice_chats`` were renamed to :paramref:`can_manage_video_chats` and :attr:`can_manage_video_chats` in accordance to Bot API 6.0. + * The argument :paramref:`can_manage_topics` was added, which changes the position of the + optional argument :paramref:`custom_title`. Args: user (:class:`telegram.User`): Information about the user. @@ -207,6 +209,10 @@ class ChatMemberAdministrator(ChatMember): messages; channels only. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. + can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only. + + .. versionadded:: 20.0 custom_title (:obj:`str`, optional): Custom title for this user. Attributes: @@ -244,6 +250,10 @@ class ChatMemberAdministrator(ChatMember): messages; channels only. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. + can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only + + .. versionadded:: 20.0 custom_title (:obj:`str`): Optional. Custom title for this user. """ @@ -260,6 +270,7 @@ class ChatMemberAdministrator(ChatMember): "can_post_messages", "can_edit_messages", "can_pin_messages", + "can_manage_topics", "custom_title", ) @@ -278,6 +289,7 @@ def __init__( can_post_messages: bool = None, can_edit_messages: bool = None, can_pin_messages: bool = None, + can_manage_topics: bool = None, custom_title: str = None, *, api_kwargs: JSONDict = None, @@ -295,6 +307,7 @@ def __init__( self.can_post_messages = can_post_messages self.can_edit_messages = can_edit_messages self.can_pin_messages = can_pin_messages + self.can_manage_topics = can_manage_topics self.custom_title = custom_title @@ -332,6 +345,9 @@ class ChatMemberRestricted(ChatMember): in the chat. Supergroups only. .. versionadded:: 13.7 + .. versionchanged:: 20.0 + The argument :paramref:`can_manage_topics` was added, which changes the position of the + optional argument :paramref:`until_date`. Args: user (:class:`telegram.User`): Information about the user. @@ -353,6 +369,10 @@ class ChatMemberRestricted(ChatMember): to send animations, games, stickers and use inline bots. can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is allowed to add web page previews to their messages. + can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create + forum topics. + + .. versionadded:: 20.0 until_date (:class:`datetime.datetime`): Date when restrictions will be lifted for this user. @@ -378,6 +398,10 @@ class ChatMemberRestricted(ChatMember): to send animations, games, stickers and use inline bots. can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is allowed to add web page previews to their messages. + can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create + forum topics. + + .. versionadded:: 20.0 until_date (:class:`datetime.datetime`): Date when restrictions will be lifted for this user. @@ -393,6 +417,7 @@ class ChatMemberRestricted(ChatMember): "can_send_polls", "can_send_other_messages", "can_add_web_page_previews", + "can_manage_topics", "until_date", ) @@ -408,6 +433,7 @@ def __init__( can_send_polls: bool, can_send_other_messages: bool, can_add_web_page_previews: bool, + can_manage_topics: bool, until_date: datetime.datetime, *, api_kwargs: JSONDict = None, @@ -422,6 +448,7 @@ def __init__( self.can_send_polls = can_send_polls self.can_send_other_messages = can_send_other_messages self.can_add_web_page_previews = can_add_web_page_previews + self.can_manage_topics = can_manage_topics self.until_date = until_date diff --git a/telegram/_chatpermissions.py b/telegram/_chatpermissions.py index 416e3306a27..c05cfacbdd4 100644 --- a/telegram/_chatpermissions.py +++ b/telegram/_chatpermissions.py @@ -28,12 +28,17 @@ class ChatPermissions(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`can_send_messages`, :attr:`can_send_media_messages`, :attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`, - :attr:`can_change_info`, :attr:`can_invite_users` and :attr:`can_pin_messages` are equal. + :attr:`can_change_info`, :attr:`can_invite_users`, :attr:`can_pin_messages`, and + :attr:`can_manage_topics` are equal. + + .. versionchanged:: 20.0 + :attr:`can_manage_topics` is considered as well when comparing objects of + this type in terms of equality. Note: Though not stated explicitly in the official docs, Telegram changes not only the permissions that are set, but also sets all the others to :obj:`False`. However, since not - documented, this behaviour may change unbeknown to PTB. + documented, this behavior may change unbeknown to PTB. Args: can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send text @@ -54,6 +59,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:: 20.0 Attributes: can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text @@ -74,6 +84,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:: 20.0 """ @@ -86,6 +101,7 @@ class ChatPermissions(TelegramObject): "can_change_info", "can_pin_messages", "can_add_web_page_previews", + "can_manage_topics", ) def __init__( @@ -98,6 +114,7 @@ def __init__( can_change_info: bool = None, can_invite_users: bool = None, can_pin_messages: bool = None, + can_manage_topics: bool = None, *, api_kwargs: JSONDict = None, ): @@ -111,6 +128,7 @@ def __init__( self.can_change_info = can_change_info self.can_invite_users = can_invite_users self.can_pin_messages = can_pin_messages + self.can_manage_topics = can_manage_topics self._id_attrs = ( self.can_send_messages, @@ -121,6 +139,7 @@ def __init__( self.can_change_info, self.can_invite_users, self.can_pin_messages, + self.can_manage_topics, ) @classmethod @@ -133,7 +152,7 @@ def all_permissions(cls) -> "ChatPermissions": .. versionadded:: 20.0 """ - return cls(True, True, True, True, True, True, True, True) + return cls(True, True, True, True, True, True, True, True, True) @classmethod def no_permissions(cls) -> "ChatPermissions": @@ -143,4 +162,4 @@ def no_permissions(cls) -> "ChatPermissions": .. versionadded:: 20.0 """ - return cls(False, False, False, False, False, False, False, False) + return cls(False, False, False, False, False, False, False, False, False) diff --git a/telegram/_forumtopic.py b/telegram/_forumtopic.py new file mode 100644 index 00000000000..3cf3fe55be7 --- /dev/null +++ b/telegram/_forumtopic.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains objects related to Telegram forum topics.""" + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class ForumTopic(TelegramObject): + """ + This object represents 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:: 20.0 + + Args: + message_thread_id (:obj:`int`): Unique identifier of the forum topic + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown + as the topic icon. + + Attributes: + message_thread_id (:obj:`int`): Unique identifier of the forum topic + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown + as the topic icon. + """ + + __slots__ = ("message_thread_id", "name", "icon_color", "icon_custom_emoji_id") + + def __init__( + self, + message_thread_id: int, + name: str, + icon_color: int, + icon_custom_emoji_id: str = None, + *, + api_kwargs: JSONDict = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.message_thread_id = message_thread_id + self.name = name + self.icon_color = icon_color + self.icon_custom_emoji_id = icon_custom_emoji_id + + self._id_attrs = (self.message_thread_id, self.name, self.icon_color) + + +class ForumTopicCreated(TelegramObject): + """ + This object represents the content of a service message about a new forum topic created in + the chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name` and :attr:`icon_color` are equal. + + .. versionadded:: 20.0 + + Args: + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown + as the topic icon. + + Attributes: + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown + as the topic icon. + """ + + __slots__ = ("name", "icon_color", "icon_custom_emoji_id") + + def __init__( + self, + name: str, + icon_color: int, + icon_custom_emoji_id: str = None, + *, + api_kwargs: JSONDict = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.name = name + self.icon_color = icon_color + self.icon_custom_emoji_id = icon_custom_emoji_id + + self._id_attrs = (self.name, self.icon_color) + + +class ForumTopicClosed(TelegramObject): + """ + This object represents a service message about a forum topic closed in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + +class ForumTopicReopened(TelegramObject): + """ + This object represents a service message about a forum topic reopened in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () diff --git a/telegram/_message.py b/telegram/_message.py index 9aad571c8c6..3765566a023 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -36,6 +36,7 @@ from telegram._files.video import Video from telegram._files.videonote import VideoNote from telegram._files.voice import Voice +from telegram._forumtopic import ForumTopicClosed, ForumTopicCreated, ForumTopicReopened from telegram._games.game import Game from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged @@ -248,6 +249,26 @@ class Message(TelegramObject): reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are represented as ordinary url buttons. + is_topic_message (:obj:`bool`, optional): :obj:`True`, if the message is sent to a forum + topic. + + .. versionadded:: 20.0 + message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which + the message belongs; for supergroups only. + + .. versionadded:: 20.0 + forum_topic_created (:class:`telegram.ForumTopicCreated`, optional): Service message: + forum topic created + + .. versionadded:: 20.0 + forum_topic_closed (:class:`telegram.ForumTopicClosed`, optional): Service message: + forum topic closed + + .. versionadded:: 20.0 + forum_topic_reopened (:class:`telegram.ForumTopicReopened`, optional): Service message: + forum topic reopened + + .. versionadded:: 20.0 Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -400,6 +421,26 @@ class Message(TelegramObject): reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are represented as ordinary url buttons. + is_topic_message (:obj:`bool`): Optional. :obj:`True`, if the message is sent to a forum + topic. + + .. versionadded:: 20.0 + message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread to which + the message belongs; for supergroups only. + + .. versionadded:: 20.0 + forum_topic_created (:class:`telegram.ForumTopicCreated`): Optional. Service message: + forum topic created + + .. versionadded:: 20.0 + forum_topic_closed (:class:`telegram.ForumTopicClosed`): Optional. Service message: + forum topic closed + + .. versionadded:: 20.0 + forum_topic_reopened (:class:`telegram.ForumTopicReopened`): Optional. Service message: + forum topic reopened + + .. versionadded:: 20.0 .. |custom_emoji_formatting_note| replace:: Custom emoji entities will currently be ignored by this function. Instead, the supplied replacement for the emoji will be used. @@ -467,6 +508,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", ) def __init__( @@ -530,6 +576,11 @@ def __init__( is_automatic_forward: bool = None, has_protected_content: bool = None, web_app_data: WebAppData = None, + is_topic_message: bool = None, + message_thread_id: int = None, + forum_topic_created: ForumTopicCreated = None, + forum_topic_closed: ForumTopicClosed = None, + forum_topic_reopened: ForumTopicReopened = None, *, api_kwargs: JSONDict = None, ): @@ -596,6 +647,11 @@ def __init__( self.video_chat_participants_invited = video_chat_participants_invited self.reply_markup = reply_markup self.web_app_data = web_app_data + self.is_topic_message = is_topic_message + self.message_thread_id = message_thread_id + self.forum_topic_created = forum_topic_created + self.forum_topic_closed = forum_topic_closed + self.forum_topic_reopened = forum_topic_reopened self._effective_attachment = DEFAULT_NONE @@ -686,6 +742,13 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Message"]: data.get("video_chat_participants_invited"), bot ) data["web_app_data"] = WebAppData.de_json(data.get("web_app_data"), bot) + data["forum_topic_closed"] = ForumTopicClosed.de_json(data.get("forum_topic_closed"), bot) + data["forum_topic_created"] = ForumTopicCreated.de_json( + data.get("forum_topic_created"), bot + ) + data["forum_topic_reopened"] = ForumTopicReopened.de_json( + data.get("forum_topic_reopened"), bot + ) return super().de_json(data=data, bot=bot) @@ -784,6 +847,7 @@ async def reply_text( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -819,6 +883,7 @@ async def reply_text( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -836,6 +901,7 @@ async def reply_markdown( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -881,6 +947,7 @@ async def reply_markdown( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -898,6 +965,7 @@ async def reply_markdown_v2( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -939,6 +1007,7 @@ async def reply_markdown_v2( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -956,6 +1025,7 @@ async def reply_html( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -997,6 +1067,7 @@ async def reply_html( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1013,6 +1084,7 @@ async def reply_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1055,6 +1127,7 @@ async def reply_media_group( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, @@ -1071,6 +1144,7 @@ async def reply_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1108,6 +1182,7 @@ async def reply_photo( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1130,6 +1205,7 @@ async def reply_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1171,6 +1247,7 @@ async def reply_audio( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1191,6 +1268,7 @@ async def reply_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1235,6 +1313,7 @@ async def reply_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_animation( @@ -1252,6 +1331,7 @@ async def reply_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1299,6 +1379,7 @@ async def reply_animation( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_sticker( @@ -1309,6 +1390,7 @@ async def reply_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1346,6 +1428,7 @@ async def reply_sticker( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_video( @@ -1364,6 +1447,7 @@ async def reply_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1411,6 +1495,7 @@ async def reply_video( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_video_note( @@ -1424,6 +1509,7 @@ async def reply_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1467,6 +1553,7 @@ async def reply_video_note( allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_voice( @@ -1481,6 +1568,7 @@ async def reply_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1525,6 +1613,7 @@ async def reply_voice( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_location( @@ -1540,6 +1629,7 @@ async def reply_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: Location = None, quote: bool = None, @@ -1584,6 +1674,7 @@ async def reply_location( proximity_alert_radius=proximity_alert_radius, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_venue( @@ -1601,6 +1692,7 @@ async def reply_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: Venue = None, quote: bool = None, @@ -1647,6 +1739,7 @@ async def reply_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_contact( @@ -1660,6 +1753,7 @@ async def reply_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: Contact = None, quote: bool = None, @@ -1702,6 +1796,7 @@ async def reply_contact( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_poll( @@ -1723,6 +1818,7 @@ async def reply_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1771,6 +1867,7 @@ async def reply_poll( allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_dice( @@ -1781,6 +1878,7 @@ async def reply_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1818,6 +1916,7 @@ async def reply_dice( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_chat_action( @@ -1860,6 +1959,7 @@ async def reply_game( reply_markup: "InlineKeyboardMarkup" = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1899,6 +1999,7 @@ async def reply_game( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_invoice( @@ -1929,6 +2030,7 @@ async def reply_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1998,6 +2100,7 @@ async def reply_invoice( max_tip_amount=max_tip_amount, suggested_tip_amounts=suggested_tip_amounts, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def forward( @@ -2005,6 +2108,7 @@ async def forward( chat_id: Union[int, str], disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2041,6 +2145,7 @@ async def forward( message_id=self.message_id, disable_notification=disable_notification, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2059,6 +2164,7 @@ async def copy( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2099,6 +2205,7 @@ async def copy( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_copy( @@ -2113,6 +2220,7 @@ async def reply_copy( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2161,6 +2269,7 @@ async def reply_copy( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def edit_text( @@ -2646,6 +2755,180 @@ async def unpin( api_kwargs=api_kwargs, ) + async def edit_forum_topic( + self, + name: str, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.edit_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().edit_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + name=name, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.close_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.close_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().close_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.reopen_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.reopen_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().reopen_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.delete_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_forum_topic_messages( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.unpin_all_forum_topic_messages( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_forum_topic_messages`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().unpin_all_forum_topic_messages( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def parse_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. diff --git a/telegram/_user.py b/telegram/_user.py index 01bf0d93a87..015ec93234e 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -376,6 +376,7 @@ async def send_message( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -404,6 +405,7 @@ async def send_message( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -422,6 +424,7 @@ async def send_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -452,6 +455,7 @@ async def send_photo( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -468,6 +472,7 @@ async def send_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -500,6 +505,7 @@ async def send_media_group( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, @@ -520,6 +526,7 @@ async def send_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -554,6 +561,7 @@ async def send_audio( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -605,6 +613,7 @@ async def send_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: "Contact" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -640,6 +649,7 @@ async def send_contact( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_dice( @@ -650,6 +660,7 @@ async def send_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -680,6 +691,7 @@ async def send_dice( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_document( @@ -695,6 +707,7 @@ async def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -732,6 +745,7 @@ async def send_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_game( @@ -742,6 +756,7 @@ async def send_game( reply_markup: "InlineKeyboardMarkup" = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -772,6 +787,7 @@ async def send_game( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_invoice( @@ -802,6 +818,7 @@ async def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -862,6 +879,7 @@ async def send_invoice( max_tip_amount=max_tip_amount, suggested_tip_amounts=suggested_tip_amounts, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_location( @@ -877,6 +895,7 @@ async def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: "Location" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -914,6 +933,7 @@ async def send_location( proximity_alert_radius=proximity_alert_radius, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_animation( @@ -931,6 +951,7 @@ async def send_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -970,6 +991,7 @@ async def send_animation( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_sticker( @@ -980,6 +1002,7 @@ async def send_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -1010,6 +1033,7 @@ async def send_sticker( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_video( @@ -1028,6 +1052,7 @@ async def send_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1068,6 +1093,7 @@ async def send_video( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_venue( @@ -1085,6 +1111,7 @@ async def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: "Venue" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1124,6 +1151,7 @@ async def send_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_video_note( @@ -1137,6 +1165,7 @@ async def send_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1172,6 +1201,7 @@ async def send_video_note( allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_voice( @@ -1186,6 +1216,7 @@ async def send_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1222,6 +1253,7 @@ async def send_voice( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_poll( @@ -1243,6 +1275,7 @@ async def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1284,6 +1317,7 @@ async def send_poll( allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_copy( @@ -1298,6 +1332,7 @@ async def send_copy( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1332,6 +1367,7 @@ async def send_copy( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def copy_message( @@ -1346,6 +1382,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1380,6 +1417,7 @@ async def copy_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def approve_join_request( diff --git a/telegram/constants.py b/telegram/constants.py index f5cd7b838f2..aba31dc4903 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -39,12 +39,15 @@ "ChatAction", "ChatID", "ChatInviteLinkLimit", + "ChatLimit", "ChatMemberStatus", "ChatType", "CustomEmojiStickerLimit", "DiceEmoji", "FileSizeLimit", "FloodLimit", + "ForumIconColor", + "ForumTopicLimit", "InlineKeyboardMarkupLimit", "InlineQueryLimit", "InlineQueryResultType", @@ -97,7 +100,7 @@ def __str__(self) -> str: #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=2) +BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=3) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -237,6 +240,34 @@ class ChatInviteLinkLimit(IntEnum): :meth:`telegram.Bot.create_chat_invite_link` and :meth:`telegram.Bot.edit_chat_invite_link`.""" +class ChatLimit(IntEnum): + """This enum contains limitations for + :meth:`telegram.Bot.set_chat_administrator_custom_title` and + :meth:`telegram.Bot.set_chat_title`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + CHAT_ADMINISTRATOR_CUSTOM_TITLE_LENGTH = 16 + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_administrator_custom_title.custom_title` parameter of + :meth:`telegram.Bot.set_chat_administrator_custom_title`. + """ + MIN_CHAT_TITLE_LENGTH = 1 + """:obj:`int`: Minimum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_title.title` parameter of + :meth:`telegram.Bot.set_chat_title`. + """ + MAX_CHAT_TITLE_LENGTH = 128 + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_title.title` parameter of + :meth:`telegram.Bot.set_chat_title`. + """ + + class ChatMemberStatus(StringEnum): """This enum contains the available states for :class:`telegram.ChatMember`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. @@ -370,6 +401,66 @@ class FloodLimit(IntEnum): """ +class ForumIconColor(IntEnum): + """This enum contains the available colors for use in + :paramref:`telegram.Bot.create_forum_topic.icon_color`. The enum members of this enumeration + are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + BLUE = 0x6FB9F0 + """:obj:`int`: An icon with a color which corresponds to blue (``0x6FB9F0``). + + .. raw:: html + +
+ + """ + YELLOW = 0xFFD67E + """:obj:`int`: An icon with a color which corresponds to yellow (``0xFFD67E``). + + .. raw:: html + +
+ + """ + PURPLE = 0xCB86DB + """:obj:`int`: An icon with a color which corresponds to purple (``0xCB86DB``). + + .. raw:: html + +
+ + """ + GREEN = 0x8EEE98 + """:obj:`int`: An icon with a color which corresponds to green (``0x8EEE98``). + + .. raw:: html + +
+ + """ + PINK = 0xFF93B2 + """:obj:`int`: An icon with a color which corresponds to pink (``0xFF93B2``). + + .. raw:: html + +
+ + """ + RED = 0xFB6F5F + """:obj:`int`: An icon with a color which corresponds to red (``0xFB6F5F``). + + .. raw:: html + +
+ + """ + + class InlineKeyboardMarkupLimit(IntEnum): """This enum contains limitations for :class:`telegram.InlineKeyboardMarkup`/ :meth:`telegram.Bot.send_message` & friends. The enum @@ -901,3 +992,27 @@ class WebhookLimit(IntEnum): """:obj:`int`: Minimum length of the secret token.""" MAX_SECRET_TOKEN_LENGTH = 256 """:obj:`int`: Maximum length of the secret token.""" + + +class ForumTopicLimit(IntEnum): + """This enum contains limitations for :paramref:`telegram.Bot.create_forum_topic.name` and + :paramref:`telegram.Bot.edit_forum_topic.name`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_NAME_LENGTH = 1 + """:obj:`int`: Minimum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.create_forum_topic.name` parameter of + :meth:`telegram.Bot.create_forum_topic` and :paramref:`~telegram.Bot.edit_forum_topic.name` + parameter of :meth:`telegram.Bot.edit_forum_topic`. + """ + MAX_NAME_LENGTH = 128 + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.create_forum_topic.name` parameter of + :meth:`telegram.Bot.create_forum_topic` and :paramref:`~telegram.Bot.edit_forum_topic.name` + parameter of :meth:`telegram.Bot.edit_forum_topic`. + """ diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index b6622108f02..0fd9713068b 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -53,6 +53,7 @@ Contact, Document, File, + ForumTopic, GameHighScore, InlineKeyboardMarkup, InputMedia, @@ -467,6 +468,7 @@ async def _send_message( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -484,6 +486,7 @@ async def _send_message( reply_markup=self._replace_keyboard(reply_markup), allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -631,6 +634,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -652,6 +656,7 @@ async def copy_message( allow_sending_without_reply=allow_sending_without_reply, reply_markup=self._replace_keyboard(reply_markup), protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1103,6 +1108,28 @@ async def delete_chat_sticker_set( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def delete_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().delete_forum_topic( + chat_id=chat_id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def delete_message( self, chat_id: Union[str, int], @@ -1217,6 +1244,32 @@ async def edit_chat_invite_link( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def edit_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + name: str, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().edit_forum_topic( + chat_id=chat_id, + message_thread_id=message_thread_id, + name=name, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def edit_message_caption( self, chat_id: Union[str, int] = None, @@ -1402,6 +1455,7 @@ async def forward_message( message_id: int, disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1416,6 +1470,7 @@ async def forward_message( message_id=message_id, disable_notification=disable_notification, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1527,6 +1582,24 @@ async def get_file( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_forum_topic_icon_stickers( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> List[Sticker]: + return await super().get_forum_topic_icon_stickers( + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def get_game_high_scores( self, user_id: Union[int, str], @@ -1751,6 +1824,54 @@ async def close( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def close_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().close_forum_topic( + chat_id=chat_id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def create_forum_topic( + self, + chat_id: Union[str, int], + name: str, + icon_color: int = None, + icon_custom_emoji_id: str = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> ForumTopic: + return await super().create_forum_topic( + chat_id=chat_id, + name=name, + icon_color=icon_color, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def pin_chat_message( self, chat_id: Union[str, int], @@ -1790,6 +1911,7 @@ async def promote_chat_member( is_anonymous: bool = None, can_manage_chat: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1812,6 +1934,29 @@ async def promote_chat_member( is_anonymous=is_anonymous, can_manage_chat=can_manage_chat, can_manage_video_chats=can_manage_video_chats, + can_manage_topics=can_manage_topics, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def reopen_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().reopen_forum_topic( + chat_id=chat_id, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1883,6 +2028,7 @@ async def send_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1907,6 +2053,7 @@ async def send_animation( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1931,6 +2078,7 @@ async def send_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1955,6 +2103,7 @@ async def send_audio( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1997,6 +2146,7 @@ async def send_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: Contact = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2017,6 +2167,7 @@ async def send_contact( vcard=vcard, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, contact=contact, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2034,6 +2185,7 @@ async def send_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2050,6 +2202,7 @@ async def send_dice( emoji=emoji, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2071,6 +2224,7 @@ async def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2093,6 +2247,7 @@ async def send_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2110,6 +2265,7 @@ async def send_game( reply_markup: InlineKeyboardMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2126,6 +2282,7 @@ async def send_game( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2162,6 +2319,7 @@ async def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2198,6 +2356,7 @@ async def send_invoice( max_tip_amount=max_tip_amount, suggested_tip_amounts=suggested_tip_amounts, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2219,6 +2378,7 @@ async def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: Location = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2241,6 +2401,7 @@ async def send_location( proximity_alert_radius=proximity_alert_radius, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, location=location, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2259,6 +2420,7 @@ async def send_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -2277,6 +2439,7 @@ async def send_media_group( reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2299,6 +2462,7 @@ async def send_message( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2315,6 +2479,7 @@ async def send_message( disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, protect_content=protect_content, + message_thread_id=message_thread_id, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2337,6 +2502,7 @@ async def send_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2357,6 +2523,7 @@ async def send_photo( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2385,6 +2552,7 @@ async def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2412,6 +2580,7 @@ async def send_poll( allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2428,6 +2597,7 @@ async def send_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -2444,6 +2614,7 @@ async def send_sticker( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2467,6 +2638,7 @@ async def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: Venue = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2491,6 +2663,7 @@ async def send_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, venue=venue, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2516,6 +2689,7 @@ async def send_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2541,6 +2715,7 @@ async def send_video( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2561,6 +2736,7 @@ async def send_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2581,6 +2757,7 @@ async def send_video_note( thumb=thumb, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2602,6 +2779,7 @@ async def send_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2623,6 +2801,7 @@ async def send_voice( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -3079,6 +3258,28 @@ async def unpin_chat_message( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def unpin_all_forum_topic_messages( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().unpin_all_forum_topic_messages( + chat_id=chat_id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def upload_sticker_file( self, user_id: Union[str, int], @@ -3190,3 +3391,10 @@ async def upload_sticker_file( getMyDefaultAdministratorRights = get_my_default_administrator_rights setMyDefaultAdministratorRights = set_my_default_administrator_rights createInvoiceLink = create_invoice_link + getForumTopicIconStickers = get_forum_topic_icon_stickers + createForumTopic = create_forum_topic + editForumTopic = edit_forum_topic + closeForumTopic = close_forum_topic + reopenForumTopic = reopen_forum_topic + deleteForumTopic = delete_forum_topic + unpinAllForumTopicMessages = unpin_all_forum_topic_messages diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 5ad236153cd..cdd4cf62ccc 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -61,6 +61,7 @@ "HAS_PROTECTED_CONTENT", "INVOICE", "IS_AUTOMATIC_FORWARD", + "IS_TOPIC_MESSAGE", "LOCATION", "Language", "MessageFilter", @@ -1422,6 +1423,20 @@ def filter(self, message: Message) -> bool: """ +class _IsTopicMessage(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.is_topic_message) + + +IS_TOPIC_MESSAGE = _IsTopicMessage(name="filters.IS_TOPIC_MESSAGE") +"""Messages that contain :attr:`telegram.Message.is_topic_message`. + + .. versionadded:: 20.0 +""" + + class Language(MessageFilter): """Filters messages to only allow those which are from users with a certain language code. @@ -1705,6 +1720,9 @@ def filter(self, update: Update) -> bool: or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update) or StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED.check_update(update) or StatusUpdate.WEB_APP_DATA.check_update(update) + or StatusUpdate.FORUM_TOPIC_CREATED.check_update(update) + or StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update) + or StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update) ) ALL = _All(name="filters.StatusUpdate.ALL") @@ -1893,6 +1911,42 @@ def filter(self, message: Message) -> bool: .. versionadded:: 20.0 """ + class _ForumTopicCreated(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_created) + + FORUM_TOPIC_CREATED = _ForumTopicCreated(name="filters.StatusUpdate.FORUM_TOPIC_CREATED") + """Messages that contain :attr:`telegram.Message.forum_topic_created`. + + .. versionadded:: 20.0 + """ + + class _ForumTopicClosed(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_closed) + + FORUM_TOPIC_CLOSED = _ForumTopicClosed(name="filters.StatusUpdate.FORUM_TOPIC_CLOSED") + """Messages that contain :attr:`telegram.Message.forum_topic_closed`. + + .. versionadded:: 20.0 + """ + + class _ForumTopicReopened(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_reopened) + + FORUM_TOPIC_REOPENED = _ForumTopicReopened(name="filters.StatusUpdate.FORUM_TOPIC_REOPENED") + """Messages that contain :attr:`telegram.Message.forum_topic_reopened`. + + .. versionadded:: 20.0 + """ + class Sticker: """Filters messages which contain a sticker. diff --git a/tests/bots.py b/tests/bots.py index 69927a32cdc..4ed5160a5ac 100644 --- a/tests/bots.py +++ b/tests/bots.py @@ -28,13 +28,14 @@ FALLBACKS = ( "W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc" "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj" - "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3J" - "hbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAxIiwgImJvdF91c2VybmFtZSI6ICJAcHRi" - "X2ZhbGxiYWNrXzFfYm90In0sIHsidG9rZW4iOiAiNTU4MTk0MDY2OkFBRndEUElGbHpHVWxDYVdIdFRPRVg0UkZyWDh1O" - "URNcWZvIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WWpFd09EUXdNVEZtTkRjeSIsIC" - "JjaGF0X2lkIjogIjY3NTY2NjIyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTIyMTIxNjgzMCIsICJjaGFubmVsX2l" - "kIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgImJv" - "dF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90In1d" + "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTYxOTE" + "1OTQwNCIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0" + "cyBmYWxsYmFjayAxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzFfYm90In0sIHsidG9rZW4iOiAiNTU4M" + "Tk0MDY2OkFBRndEUElGbHpHVWxDYVdIdFRPRVg0UkZyWDh1OURNcWZvIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOi" + "AiMjg0Njg1MDYzOlRFU1Q6WWpFd09EUXdNVEZtTkRjeSIsICJjaGF0X2lkIjogIjY3NTY2NjIyNCIsICJzdXBlcl9ncm9" + "1cF9pZCI6ICItMTAwMTIyMTIxNjgzMCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTYxOTE1OTQwNCIsICJjaGFubmVs" + "X2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgI" + "mJvdF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90In1d " ) GITHUB_ACTION = os.getenv("GITHUB_ACTION", None) diff --git a/tests/conftest.py b/tests/conftest.py index 81b611b1da6..9d56aab7619 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -207,6 +207,11 @@ def super_group_id(bot_info): return bot_info["super_group_id"] +@pytest.fixture(scope="session") +def forum_group_id(bot_info): + return int(bot_info["forum_group_id"]) + + @pytest.fixture(scope="session") def channel_id(bot_info): return bot_info["channel_id"] diff --git a/tests/test_bot.py b/tests/test_bot.py index ebfbb5b7c72..e042f67bbd3 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2259,6 +2259,7 @@ async def test_promote_chat_member(self, bot, channel_id, monkeypatch): can_promote_members=True, can_manage_chat=True, can_manage_video_chats=True, + can_manage_topics=True, ) # Test that we pass the correct params to TG @@ -2278,6 +2279,7 @@ async def make_assertion(*args, **_): and data.get("can_promote_members") == 9 and data.get("can_manage_chat") == 10 and data.get("can_manage_video_chats") == 11 + and data.get("can_manage_topics") == 12 ) monkeypatch.setattr(bot, "_post", make_assertion) @@ -2295,6 +2297,7 @@ async def make_assertion(*args, **_): can_promote_members=9, can_manage_chat=10, can_manage_video_chats=11, + can_manage_topics=12, ) @pytest.mark.flaky(3, 1) @@ -2565,6 +2568,9 @@ async def test_pin_and_unpin_message(self, bot, super_group_id): # set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers # are tested in the test_sticker module. + # get_forum_topic_icon_stickers, edit_forum_topic, etc... + # are tested in the test_forum module. + async def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id): # Use BaseException that's not a subclass of Exception such that # OkException should not be caught anywhere @@ -2709,6 +2715,7 @@ async def test_get_set_my_default_administrator_rights(self, bot): assert my_admin_rights_ch.can_promote_members is my_rights.can_promote_members assert my_admin_rights_ch.can_restrict_members is my_rights.can_restrict_members assert my_admin_rights_ch.can_pin_messages is None # Not returned for channels + assert my_admin_rights_ch.can_manage_topics is None # Not returned for channels @pytest.mark.asyncio async def test_get_set_chat_menu_button(self, bot, chat_id): @@ -2822,6 +2829,7 @@ async def post(url, request_data: RequestData, *args, **kwargs): data["caption_entities"] == [MessageEntity(MessageEntity.BOLD, 0, 4).to_dict()], data["protect_content"] is True, + data["message_thread_id"] == 1, ] ): pytest.fail("I got wrong parameters in post") @@ -2839,6 +2847,7 @@ async def post(url, request_data: RequestData, *args, **kwargs): reply_markup=keyboard.to_json() if json_keyboard else keyboard, disable_notification=True, protect_content=True, + message_thread_id=1, ) @pytest.mark.flaky(3, 1) diff --git a/tests/test_chat.py b/tests/test_chat.py index ddc9d3cdbd8..e8871deeedd 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -44,6 +44,9 @@ def chat(bot): join_to_send_messages=True, join_by_request=True, has_restricted_voice_and_video_messages=True, + is_forum=True, + active_usernames=TestChat.active_usernames, + emoji_status_custom_emoji_id=TestChat.emoji_status_custom_emoji_id, ) chat.set_bot(bot) return chat @@ -71,6 +74,9 @@ class TestChat: join_to_send_messages = True join_by_request = True has_restricted_voice_and_video_messages = True + is_forum = True + active_usernames = ["These", "Are", "Usernames!"] + emoji_status_custom_emoji_id = "VeryUniqueCustomEmojiID" def test_slot_behaviour(self, chat, mro_slots): for attr in chat.__slots__: @@ -98,6 +104,9 @@ def test_de_json(self, bot): "has_restricted_voice_and_video_messages": ( self.has_restricted_voice_and_video_messages ), + "is_forum": self.is_forum, + "active_usernames": self.active_usernames, + "emoji_status_custom_emoji_id": self.emoji_status_custom_emoji_id, } chat = Chat.de_json(json_dict, bot) @@ -124,6 +133,9 @@ def test_de_json(self, bot): assert chat.api_kwargs == { "all_members_are_administrators": self.all_members_are_administrators } + assert chat.is_forum == self.is_forum + assert chat.active_usernames == self.active_usernames + assert chat.emoji_status_custom_emoji_id == self.emoji_status_custom_emoji_id def test_to_dict(self, chat): chat_dict = chat.to_dict() @@ -146,6 +158,9 @@ def test_to_dict(self, chat): chat_dict["has_restricted_voice_and_video_messages"] == chat.has_restricted_voice_and_video_messages ) + assert chat_dict["is_forum"] == chat.is_forum + assert chat_dict["active_usernames"] == chat.active_usernames + assert chat_dict["emoji_status_custom_emoji_id"] == chat.emoji_status_custom_emoji_id def test_enum_init(self): chat = Chat(id=1, type="foo") @@ -881,6 +896,128 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "decline_chat_join_request", make_assertion) assert await chat.decline_join_request(user_id=42) + async def test_create_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["name"] == "New Name" + and kwargs["icon_color"] == 0x6FB9F0 + and kwargs["icon_custom_emoji_id"] == "12345" + ) + + assert check_shortcut_signature( + Chat.create_forum_topic, Bot.create_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.create_forum_topic, + chat.get_bot(), + "create_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.create_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "create_forum_topic", make_assertion) + assert await chat.create_forum_topic( + name="New Name", icon_color=0x6FB9F0, icon_custom_emoji_id="12345" + ) + + async def test_edit_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["message_thread_id"] == 42 + and kwargs["name"] == "New Name" + and kwargs["icon_custom_emoji_id"] == "12345" + ) + + assert check_shortcut_signature( + Chat.edit_forum_topic, Bot.edit_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.edit_forum_topic, chat.get_bot(), "edit_forum_topic", shortcut_kwargs=["chat_id"] + ) + assert await check_defaults_handling(chat.edit_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "edit_forum_topic", make_assertion) + assert await chat.edit_forum_topic( + message_thread_id=42, name="New Name", icon_custom_emoji_id="12345" + ) + + async def test_close_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.close_forum_topic, Bot.close_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.close_forum_topic, + chat.get_bot(), + "close_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.close_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "close_forum_topic", make_assertion) + assert await chat.close_forum_topic(message_thread_id=42) + + async def test_reopen_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.reopen_forum_topic, Bot.reopen_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.reopen_forum_topic, + chat.get_bot(), + "reopen_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.reopen_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "reopen_forum_topic", make_assertion) + assert await chat.reopen_forum_topic(message_thread_id=42) + + async def test_delete_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.delete_forum_topic, Bot.delete_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.delete_forum_topic, + chat.get_bot(), + "delete_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.delete_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "delete_forum_topic", make_assertion) + assert await chat.delete_forum_topic(message_thread_id=42) + + async def test_unpin_all_forum_topic_messages(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.unpin_all_forum_topic_messages, + Bot.unpin_all_forum_topic_messages, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.unpin_all_forum_topic_messages, + chat.get_bot(), + "unpin_all_forum_topic_messages", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.unpin_all_forum_topic_messages, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unpin_all_forum_topic_messages", make_assertion) + assert await chat.unpin_all_forum_topic_messages(message_thread_id=42) + def test_mention_html(self): with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): chat = Chat(id=1, type="foo") diff --git a/tests/test_chatadministratorrights.py b/tests/test_chatadministratorrights.py index 16175f252a6..83826f74bac 100644 --- a/tests/test_chatadministratorrights.py +++ b/tests/test_chatadministratorrights.py @@ -34,6 +34,7 @@ def chat_admin_rights(): can_edit_messages=True, can_manage_chat=True, can_manage_video_chats=True, + can_manage_topics=True, is_anonymous=True, ) @@ -57,6 +58,7 @@ def test_de_json(self, bot, chat_admin_rights): "can_edit_messages": True, "can_manage_chat": True, "can_manage_video_chats": True, + "can_manage_topics": True, "is_anonymous": True, } chat_administrator_rights_de = ChatAdministratorRights.de_json(json_dict, bot) @@ -80,13 +82,14 @@ def test_to_dict(self, chat_admin_rights): assert admin_rights_dict["can_manage_chat"] == car.can_manage_chat assert admin_rights_dict["is_anonymous"] == car.is_anonymous assert admin_rights_dict["can_manage_video_chats"] == car.can_manage_video_chats + assert admin_rights_dict["can_manage_topics"] == car.can_manage_topics def test_equality(self): - a = ChatAdministratorRights(True, False, False, False, False, False, False, False) - b = ChatAdministratorRights(True, False, False, False, False, False, False, False) - c = ChatAdministratorRights(False, False, False, False, False, False, False, False) - d = ChatAdministratorRights(True, True, False, False, False, False, False, False) - e = ChatAdministratorRights(True, True, False, False, False, False, False, False) + a = ChatAdministratorRights(True, False, False, False, False, False, False, False, False) + b = ChatAdministratorRights(True, False, False, False, False, False, False, False, False) + c = ChatAdministratorRights(False, False, False, False, False, False, False, False, False) + d = ChatAdministratorRights(True, True, False, False, False, False, False, False, False) + e = ChatAdministratorRights(True, True, False, False, False, False, False, False, False) assert a == b assert hash(a) == hash(b) @@ -102,7 +105,7 @@ def test_equality(self): assert hash(d) == hash(e) def test_all_rights(self): - f = ChatAdministratorRights(True, True, True, True, True, True, True, True) + f = ChatAdministratorRights(True, True, True, True, True, True, True, True, True) t = ChatAdministratorRights.all_rights() # if the dirs are the same, the attributes will all be there assert dir(f) == dir(t) @@ -114,7 +117,7 @@ def test_all_rights(self): assert f != t def test_no_rights(self): - f = ChatAdministratorRights(False, False, False, False, False, False, False, False) + f = ChatAdministratorRights(False, False, False, False, False, False, False, False, False) t = ChatAdministratorRights.no_rights() # if the dirs are the same, the attributes will all be there assert dir(f) == dir(t) diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index f2a1d0854c8..ab7e7d23da2 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -60,6 +60,7 @@ class CMDefaults: is_member: bool = True can_manage_chat: bool = True can_manage_video_chats: bool = True + can_manage_topics: bool = True def chat_member_owner(): @@ -81,6 +82,7 @@ def chat_member_administrator(): CMDefaults.can_post_messages, CMDefaults.can_edit_messages, CMDefaults.can_pin_messages, + CMDefaults.can_manage_topics, CMDefaults.custom_title, ) @@ -101,6 +103,7 @@ def chat_member_restricted(): CMDefaults.can_send_polls, CMDefaults.can_send_other_messages, CMDefaults.can_add_web_page_previews, + CMDefaults.can_manage_topics, CMDefaults.until_date, ) diff --git a/tests/test_chatpermissions.py b/tests/test_chatpermissions.py index 64f4da6c269..5498bae24ca 100644 --- a/tests/test_chatpermissions.py +++ b/tests/test_chatpermissions.py @@ -33,6 +33,7 @@ def chat_permissions(): can_change_info=True, can_invite_users=True, can_pin_messages=True, + can_manage_topics=True, ) @@ -45,6 +46,7 @@ class TestChatPermissions: can_change_info = False can_invite_users = None can_pin_messages = None + can_manage_topics = None def test_slot_behaviour(self, chat_permissions, mro_slots): inst = chat_permissions @@ -62,6 +64,7 @@ def test_de_json(self, bot): "can_change_info": self.can_change_info, "can_invite_users": self.can_invite_users, "can_pin_messages": self.can_pin_messages, + "can_manage_topics": self.can_manage_topics, } permissions = ChatPermissions.de_json(json_dict, bot) assert permissions.api_kwargs == {} @@ -74,6 +77,7 @@ def test_de_json(self, bot): assert permissions.can_change_info == self.can_change_info assert permissions.can_invite_users == self.can_invite_users assert permissions.can_pin_messages == self.can_pin_messages + assert permissions.can_manage_topics == self.can_manage_topics def test_to_dict(self, chat_permissions): permissions_dict = chat_permissions.to_dict() @@ -94,6 +98,7 @@ def test_to_dict(self, chat_permissions): assert permissions_dict["can_change_info"] == chat_permissions.can_change_info assert permissions_dict["can_invite_users"] == chat_permissions.can_invite_users assert permissions_dict["can_pin_messages"] == chat_permissions.can_pin_messages + assert permissions_dict["can_manage_topics"] == chat_permissions.can_manage_topics def test_equality(self): a = ChatPermissions( diff --git a/tests/test_filters.py b/tests/test_filters.py index a3fcb5c3280..a79a7be0916 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -126,6 +126,22 @@ def filter_class(obj): for attr in cls.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}' for {name}" + def test__all__(self): + expected = { + key + for key, member in filters.__dict__.items() + if ( + not key.startswith("_") + # exclude imported stuff + and getattr(member, "__module__", "unknown module") == "telegram.ext.filters" + and key != "sys" + ) + } + actual = set(filters.__all__) + assert ( + actual == expected + ), f"Members {expected - actual} were not listed in constants.__all__" + def test_filters_all(self, update): assert filters.ALL.check_update(update) @@ -980,6 +996,21 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.WEB_APP_DATA.check_update(update) update.message.web_app_data = None + update.message.forum_topic_created = "topic" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.FORUM_TOPIC_CREATED.check_update(update) + update.message.forum_topic_created = None + + update.message.forum_topic_closed = "topic" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update) + update.message.forum_topic_closed = None + + update.message.forum_topic_reopened = "topic" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update) + update.message.forum_topic_reopened = None + def test_filters_forwarded(self, update): assert not filters.FORWARDED.check_update(update) update.message.forward_date = datetime.datetime.utcnow() @@ -1764,6 +1795,11 @@ def test_filters_is_automatic_forward(self, update): update.message.is_automatic_forward = True assert filters.IS_AUTOMATIC_FORWARD.check_update(update) + def test_filters_is_topic_message(self, update): + assert not filters.IS_TOPIC_MESSAGE.check_update(update) + update.message.is_topic_message = True + assert filters.IS_TOPIC_MESSAGE.check_update(update) + def test_filters_has_protected_content(self, update): assert not filters.HAS_PROTECTED_CONTENT.check_update(update) update.message.has_protected_content = True diff --git a/tests/test_forum.py b/tests/test_forum.py new file mode 100644 index 00000000000..d903d3e3d1a --- /dev/null +++ b/tests/test_forum.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import pytest + +from telegram import ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicReopened, Sticker + +TEST_MSG_TEXT = "Topics are forever" +TEST_TOPIC_ICON_COLOR = 0x6FB9F0 +TEST_TOPIC_NAME = "Sad bot true: real stories" + + +@pytest.fixture(scope="module") +async def emoji_id(bot): + emoji_sticker_list = await bot.get_forum_topic_icon_stickers() + first_sticker = emoji_sticker_list[0] + return first_sticker.custom_emoji_id + + +@pytest.fixture +async def forum_topic_object(forum_group_id, emoji_id): + return ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + + +@pytest.fixture +async def real_topic(bot, emoji_id, forum_group_id): + result = await bot.create_forum_topic( + chat_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + + yield result + + result = await bot.delete_forum_topic( + chat_id=forum_group_id, message_thread_id=result.message_thread_id + ) + assert result is True, "Topic was not deleted" + + +class TestForumTopic: + def test_slot_behaviour(self, mro_slots, forum_topic_object): + for attr in forum_topic_object.__slots__: + assert getattr(forum_topic_object, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(forum_topic_object)) == len( + set(mro_slots(forum_topic_object)) + ), "duplicate slot" + + async def test_expected_values(self, emoji_id, forum_group_id, forum_topic_object): + assert forum_topic_object.message_thread_id == forum_group_id + assert forum_topic_object.icon_color == TEST_TOPIC_ICON_COLOR + assert forum_topic_object.name == TEST_TOPIC_NAME + assert forum_topic_object.icon_custom_emoji_id == emoji_id + + def test_de_json(self, bot, emoji_id, forum_group_id): + assert ForumTopic.de_json(None, bot=bot) is None + + json_dict = { + "message_thread_id": forum_group_id, + "name": TEST_TOPIC_NAME, + "icon_color": TEST_TOPIC_ICON_COLOR, + "icon_custom_emoji_id": emoji_id, + } + topic = ForumTopic.de_json(json_dict, bot) + assert topic.api_kwargs == {} + + assert topic.message_thread_id == forum_group_id + assert topic.icon_color == TEST_TOPIC_ICON_COLOR + assert topic.name == TEST_TOPIC_NAME + assert topic.icon_custom_emoji_id == emoji_id + + def test_to_dict(self, emoji_id, forum_group_id, forum_topic_object): + topic_dict = forum_topic_object.to_dict() + + assert isinstance(topic_dict, dict) + assert topic_dict["message_thread_id"] == forum_group_id + assert topic_dict["name"] == TEST_TOPIC_NAME + assert topic_dict["icon_color"] == TEST_TOPIC_ICON_COLOR + assert topic_dict["icon_custom_emoji_id"] == emoji_id + + def test_equality(self, emoji_id, forum_group_id): + a = ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + ) + b = ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + c = ForumTopic( + message_thread_id=forum_group_id, + name=f"{TEST_TOPIC_NAME}!", + icon_color=TEST_TOPIC_ICON_COLOR, + ) + d = ForumTopic( + message_thread_id=forum_group_id + 1, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + ) + e = ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=0xFFD67E, + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + @pytest.mark.flaky(3, 1) + async def test_create_forum_topic(self, real_topic): + result = real_topic + assert isinstance(result, ForumTopic) + assert result.name == TEST_TOPIC_NAME + assert result.message_thread_id + assert isinstance(result.icon_color, int) + assert isinstance(result.icon_custom_emoji_id, str) + + async def test_create_forum_topic_with_only_required_args(self, bot, forum_group_id): + result = await bot.create_forum_topic(chat_id=forum_group_id, name=TEST_TOPIC_NAME) + assert isinstance(result, ForumTopic) + assert result.name == TEST_TOPIC_NAME + assert result.message_thread_id + assert isinstance(result.icon_color, int) # color is still there though it was not passed + assert result.icon_custom_emoji_id is None + + result = await bot.delete_forum_topic( + chat_id=forum_group_id, message_thread_id=result.message_thread_id + ) + assert result is True, "Failed to delete forum topic" + + @pytest.mark.flaky(3, 1) + async def test_get_forum_topic_icon_stickers(self, bot): + emoji_sticker_list = await bot.get_forum_topic_icon_stickers() + first_sticker = emoji_sticker_list[0] + + assert first_sticker.emoji == "📰" + assert first_sticker.height == 512 + assert first_sticker.width == 512 + assert first_sticker.is_animated + assert not first_sticker.is_video + assert first_sticker.set_name == "Topics" + assert first_sticker.type == Sticker.CUSTOM_EMOJI + assert first_sticker.thumb.width == 128 + assert first_sticker.thumb.height == 128 + + # The following data of first item returned has changed in the past already, + # so check sizes loosely and ID's only by length of string + assert first_sticker.thumb.file_size in range(2000, 7000) + assert first_sticker.file_size in range(20000, 70000) + assert len(first_sticker.custom_emoji_id) == 19 + assert len(first_sticker.thumb.file_unique_id) == 16 + assert len(first_sticker.file_unique_id) == 15 + + async def test_edit_forum_topic(self, emoji_id, forum_group_id, bot, real_topic): + result = await bot.edit_forum_topic( + chat_id=forum_group_id, + message_thread_id=real_topic.message_thread_id, + name=f"{TEST_TOPIC_NAME}_EDITED", + icon_custom_emoji_id=emoji_id, + ) + assert result is True, "Failed to edit forum topic" + # no way of checking the edited name, just the boolean result + + @pytest.mark.flaky(3, 1) + async def test_send_message_to_topic(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + message = await bot.send_message( + chat_id=forum_group_id, text=TEST_MSG_TEXT, message_thread_id=message_thread_id + ) + + assert message.text == TEST_MSG_TEXT + assert message.is_topic_message is True + assert message.message_thread_id == message_thread_id + + async def test_close_and_reopen_forum_topic(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + result = await bot.close_forum_topic( + chat_id=forum_group_id, + message_thread_id=message_thread_id, + ) + assert result is True, "Failed to close forum topic" + # bot will still be able to send a message to a closed topic, so can't test anything like + # the inability to post to the topic + + result = await bot.reopen_forum_topic( + chat_id=forum_group_id, + message_thread_id=message_thread_id, + ) + assert result is True, "Failed to reopen forum topic" + + @pytest.mark.xfail(reason="Can fail due to race conditions in GH actions CI") + async def test_unpin_all_forum_topic_messages(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + msgs = [ + await ( + await bot.send_message( + chat_id=forum_group_id, text=TEST_MSG_TEXT, message_thread_id=message_thread_id + ) + ).pin() + for _ in range(2) + ] + + assert all(msgs) is True, "Message(s) were not pinned" + + # We need 2 or more pinned msgs for this to work, else we get Chat_not_modified error + result = await bot.unpin_all_forum_topic_messages( + chat_id=forum_group_id, message_thread_id=message_thread_id + ) + assert result is True, "Failed to unpin all the messages in forum topic" + + +@pytest.fixture +def topic_created(): + return ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) + + +class TestForumTopicCreated: + def test_slot_behaviour(self, topic_created, mro_slots): + for attr in topic_created.__slots__: + assert getattr(topic_created, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(topic_created)) == len( + set(mro_slots(topic_created)) + ), "duplicate slot" + + def test_expected_values(self, topic_created): + assert topic_created.icon_color == TEST_TOPIC_ICON_COLOR + assert topic_created.name == TEST_TOPIC_NAME + + def test_de_json(self, bot): + assert ForumTopicCreated.de_json(None, bot=bot) is None + + json_dict = {"icon_color": TEST_TOPIC_ICON_COLOR, "name": TEST_TOPIC_NAME} + action = ForumTopicCreated.de_json(json_dict, bot) + assert action.api_kwargs == {} + + assert action.icon_color == TEST_TOPIC_ICON_COLOR + assert action.name == TEST_TOPIC_NAME + + def test_to_dict(self, topic_created): + action_dict = topic_created.to_dict() + + assert isinstance(action_dict, dict) + assert action_dict["name"] == TEST_TOPIC_NAME + assert action_dict["icon_color"] == TEST_TOPIC_ICON_COLOR + + def test_equality(self, emoji_id): + a = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) + b = ForumTopicCreated( + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + c = ForumTopicCreated(name=f"{TEST_TOPIC_NAME}!", icon_color=TEST_TOPIC_ICON_COLOR) + d = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=0xFFD67E) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +class TestForumTopicClosed: + def test_slot_behaviour(self, mro_slots): + action = ForumTopicClosed() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = ForumTopicClosed.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, ForumTopicClosed) + + def test_to_dict(self): + action = ForumTopicClosed() + action_dict = action.to_dict() + assert action_dict == {} + + +class TestForumTopicReopened: + def test_slot_behaviour(self, mro_slots): + action = ForumTopicReopened() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = ForumTopicReopened.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, ForumTopicReopened) + + def test_to_dict(self): + action = ForumTopicReopened() + action_dict = action.to_dict() + assert action_dict == {} diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index 897a9ebcd5e..2f261d19068 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -45,6 +45,9 @@ # noinspection PyUnresolvedReferences from .test_document import document, document_file # noqa: F401 +# noinspection PyUnresolvedReferences +from .test_forum import emoji_id, real_topic # noqa: F401 + # noinspection PyUnresolvedReferences from .test_photo import _photo, photo, photo_file, thumb # noqa: F401 @@ -465,6 +468,19 @@ async def test_send_media_group_photo(self, bot, chat_id, media_group): mes.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] for mes in messages ) + async def test_send_media_group_with_message_thread_id( + self, bot, real_topic, forum_group_id, media_group # noqa: F811 + ): + messages = await bot.send_media_group( + forum_group_id, + media_group, + message_thread_id=real_topic.message_thread_id, + ) + assert isinstance(messages, list) + assert len(messages) == 3 + assert all(isinstance(mes, Message) for mes in messages) + assert all(i.message_thread_id == real_topic.message_thread_id for i in messages) + async def test_send_media_group_throws_error_with_group_caption_and_individual_captions( self, bot, diff --git a/tests/test_message.py b/tests/test_message.py index 3ad03e386cc..bb8aa8519c6 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -198,6 +198,7 @@ def message(bot): ] }, {"web_app_data": WebAppData("some_data", "some_button_text")}, + {"message_thread_id": 123}, ], ids=[ "forwarded_user", @@ -251,6 +252,7 @@ def message(bot): "has_protected_content", "entities", "web_app_data", + "message_thread_id", ], ) def message_params(bot, request): @@ -1693,6 +1695,124 @@ def test_default_quote(self, message): finally: message.get_bot()._defaults = None + async def test_edit_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + and kwargs["name"] == "New Name" + and kwargs["icon_custom_emoji_id"] == "12345" + ) + + assert check_shortcut_signature( + Message.edit_forum_topic, Bot.edit_forum_topic, ["chat_id", "message_thread_id"], [] + ) + assert await check_shortcut_call( + message.edit_forum_topic, + message.get_bot(), + "edit_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.edit_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "edit_forum_topic", make_assertion) + assert await message.edit_forum_topic(name="New Name", icon_custom_emoji_id="12345") + + async def test_close_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.close_forum_topic, Bot.close_forum_topic, ["chat_id", "message_thread_id"], [] + ) + assert await check_shortcut_call( + message.close_forum_topic, + message.get_bot(), + "close_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.close_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "close_forum_topic", make_assertion) + assert await message.close_forum_topic() + + async def test_reopen_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.reopen_forum_topic, + Bot.reopen_forum_topic, + ["chat_id", "message_thread_id"], + [], + ) + assert await check_shortcut_call( + message.reopen_forum_topic, + message.get_bot(), + "reopen_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.reopen_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "reopen_forum_topic", make_assertion) + assert await message.reopen_forum_topic() + + async def test_delete_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.delete_forum_topic, + Bot.delete_forum_topic, + ["chat_id", "message_thread_id"], + [], + ) + assert await check_shortcut_call( + message.delete_forum_topic, + message.get_bot(), + "delete_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.delete_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "delete_forum_topic", make_assertion) + assert await message.delete_forum_topic() + + async def test_unpin_all_forum_topic_messages(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.unpin_all_forum_topic_messages, + Bot.unpin_all_forum_topic_messages, + ["chat_id", "message_thread_id"], + [], + ) + assert await check_shortcut_call( + message.unpin_all_forum_topic_messages, + message.get_bot(), + "unpin_all_forum_topic_messages", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling( + message.unpin_all_forum_topic_messages, message.get_bot() + ) + + monkeypatch.setattr(message.get_bot(), "unpin_all_forum_topic_messages", make_assertion) + assert await message.unpin_all_forum_topic_messages() + def test_equality(self): id_ = 1 a = Message(