diff --git a/changes/unreleased/5078.FoNwUYLbXQFRebTFhR6UPn.toml b/changes/unreleased/5078.FoNwUYLbXQFRebTFhR6UPn.toml index a8722a24b8c..2a4a3549b0f 100644 --- a/changes/unreleased/5078.FoNwUYLbXQFRebTFhR6UPn.toml +++ b/changes/unreleased/5078.FoNwUYLbXQFRebTFhR6UPn.toml @@ -2,5 +2,6 @@ features = "Full Support for Bot API 9.3" pull_requests = [ { uid = "5078", author_uid = "aelkheir", closes_threads = ["5077"] }, + { uid = "5079", author_uid = "aelkheir" }, { uid = "5085", author_uids = ["Bibo-Joshi"] }, ] diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 128fa26ac5c..0f2202bbbf5 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -35,6 +35,8 @@ - Used for sending media grouped together * - :meth:`~telegram.Bot.send_message` - Used for sending text messages + * - :meth:`~telegram.Bot.send_message_draft` + - Used for streaming partial text messages * - :meth:`~telegram.Bot.send_paid_media` - Used for sending paid media to channels * - :meth:`~telegram.Bot.send_photo` diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index fe7b976f5b7..f1bad363716 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -30,7 +30,7 @@ .. |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. +.. |message_thread_id_arg| replace:: Unique identifier for the target message thread (topic) of a forum; for forum supergroups and private chats of bots with forum topic mode enabled only. .. |parse_mode| replace:: Mode for parsing entities. See :class:`telegram.constants.ParseMode` and `formatting options `__ for more details. diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 5e858ec2bf3..322c4d830f3 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -1200,6 +1200,69 @@ async def delete_message( api_kwargs=api_kwargs, ) + async def send_message_draft( + self, + chat_id: int, + draft_id: int, + text: str, + message_thread_id: int | None = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = 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 = None, + ) -> bool: + """Use this method to stream a partial message to a user while the message is being + generated; supported only for bots with forum topic mode enabled. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int`): Unique identifier for the target private chat. + draft_id (:obj:`int`): Unique identifier of the message draft; must be non-zero. + Changes of drafts with the same identifier are animated. + text (:obj:`str`): Text of the message to be sent, + :tg-const:`telegram.constants.MessageLimit.MIN_TEXT_LENGTH`- + :tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`): |parse_mode| + entities (Sequence[:class:`telegram.MessageEntity`], optional): Sequence of special + entities that appear in message text, which can be specified instead of + :paramref:`parse_mode`. + + |sequenceargs| + message_thread_id (:obj:`int`, optional): Unique identifier for the target + message thread. + + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "draft_id": draft_id, + "text": text, + "entities": entities, + } + return await self._send_message( + "sendMessageDraft", + data, + message_thread_id=message_thread_id, + parse_mode=parse_mode, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def delete_messages( self, chat_id: int | str, @@ -3721,7 +3784,9 @@ async def send_chat_action( action(:obj:`str`): Type of action to broadcast. Choose one, depending on what the user is about to receive. For convenience look at the constants in :class:`telegram.constants.ChatAction`. - message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread or topic of a forum; for supergroups and private chats of bots with forum + topic mode enabled only .. versionadded:: 20.0 business_connection_id (:obj:`str`, optional): |business_id_str| @@ -8905,8 +8970,9 @@ async def edit_forum_topic( api_kwargs: JSONDict | None = 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 the + Use this method to edit name and icon of a topic in a forum supergroup chat or a private + chat with a user. In the case of a supergroup chat the bot must be an administrator in the + chat for this to work and must have the :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, unless it is the creator of the topic. @@ -9048,7 +9114,8 @@ async def delete_forum_topic( ) -> 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 + chat or a private chat with a user. In the case of a supergroup chat the bot must be an + administrator in the chat for this to work and must have the :paramref:`~telegram.ChatAdministratorRights.can_delete_messages` administrator rights. .. versionadded:: 20.0 @@ -9090,10 +9157,11 @@ async def unpin_all_forum_topic_messages( api_kwargs: JSONDict | None = 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. + Use this method to clear the list of pinned messages in a forum topic in a forum supergroup + chat or a private chat with a user. In the case of a supergroup chat the bot must be an + administrator in the chat for this to work and must have the + :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` administrator right in + the supergroup. .. versionadded:: 20.0 @@ -11591,6 +11659,8 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`get_me`""" sendMessage = send_message """Alias for :meth:`send_message`""" + sendMessageDraft = send_message_draft + """Alias for :meth:`send_message_draft`""" deleteMessage = delete_message """Alias for :meth:`delete_message`""" deleteMessages = delete_messages diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index cddaac5c1cb..659400057b1 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -1080,6 +1080,44 @@ async def send_message( suggested_post_parameters=suggested_post_parameters, ) + async def send_message_draft( + self, + draft_id: int, + text: str, + message_thread_id: int | None = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = 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 = None, + ) -> bool: + """Shortcut for:: + + await bot.send_message_draft(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message_draft`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().send_message_draft( + chat_id=self.id, + draft_id=draft_id, + text=text, + message_thread_id=message_thread_id, + parse_mode=parse_mode, + entities=entities, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def delete_message( self, message_id: int, diff --git a/src/telegram/_forumtopic.py b/src/telegram/_forumtopic.py index b648b09679d..7c787ca8c60 100644 --- a/src/telegram/_forumtopic.py +++ b/src/telegram/_forumtopic.py @@ -38,6 +38,10 @@ class ForumTopic(TelegramObject): 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. + is_name_implicit (:obj:`bool`, optional): :obj:`True`, if the name of the topic wasn't + specified explicitly by its creator and likely needs to be changed by the bot. + + .. versionadded:: NEXT.VERSION Attributes: message_thread_id (:obj:`int`): Unique identifier of the forum topic @@ -45,9 +49,19 @@ class ForumTopic(TelegramObject): 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. + is_name_implicit (:obj:`bool`): Optional. :obj:`True`, if the name of the topic wasn't + specified explicitly by its creator and likely needs to be changed by the bot. + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("icon_color", "icon_custom_emoji_id", "message_thread_id", "name") + __slots__ = ( + "icon_color", + "icon_custom_emoji_id", + "is_name_implicit", + "message_thread_id", + "name", + ) def __init__( self, @@ -55,6 +69,7 @@ def __init__( name: str, icon_color: int, icon_custom_emoji_id: str | None = None, + is_name_implicit: bool | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -63,6 +78,7 @@ def __init__( self.name: str = name self.icon_color: int = icon_color self.icon_custom_emoji_id: str | None = icon_custom_emoji_id + self.is_name_implicit: bool | None = is_name_implicit self._id_attrs = (self.message_thread_id, self.name, self.icon_color) @@ -84,21 +100,30 @@ class ForumTopicCreated(TelegramObject): 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. + is_name_implicit (:obj:`bool`, optional): :obj:`True`, if the name of the topic wasn't + specified explicitly by its creator and likely needs to be changed by the bot. + + .. versionadded:: NEXT.VERSION 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. + is_name_implicit (:obj:`bool`): Optional. :obj:`True`, if the name of the topic wasn't + specified explicitly by its creator and likely needs to be changed by the bot. + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("icon_color", "icon_custom_emoji_id", "name") + __slots__ = ("icon_color", "icon_custom_emoji_id", "is_name_implicit", "name") def __init__( self, name: str, icon_color: int, icon_custom_emoji_id: str | None = None, + is_name_implicit: bool | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -106,6 +131,7 @@ def __init__( self.name: str = name self.icon_color: int = icon_color self.icon_custom_emoji_id: str | None = icon_custom_emoji_id + self.is_name_implicit: bool | None = is_name_implicit self._id_attrs = (self.name, self.icon_color) diff --git a/src/telegram/_message.py b/src/telegram/_message.py index bccb270898e..08c37cf12a5 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -496,12 +496,12 @@ class Message(MaybeInaccessibleMessage): 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. + is_topic_message (:obj:`bool`, optional): :obj:`True`, if the message is sent to a topic + in a forum supergroup or a private chat with the bot. .. versionadded:: 20.0 - message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which - the message belongs; for supergroups only. + message_thread_id (:obj:`int`, optional): Unique identifier of a message thread or forum + topic to which the message belongs; for supergroups and private chats only. .. versionadded:: 20.0 forum_topic_created (:class:`telegram.ForumTopicCreated`, optional): Service message: @@ -898,12 +898,12 @@ class Message(MaybeInaccessibleMessage): 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. + is_topic_message (:obj:`bool`): Optional. :obj:`True`, if the message is sent to a topic + in a forum supergroup or a private chat with the bot. .. versionadded:: 20.0 - message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread to which - the message belongs; for supergroups only. + message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread or forum + topic to which the message belongs; for supergroups and private chats only. .. versionadded:: 20.0 forum_topic_created (:class:`telegram.ForumTopicCreated`): Optional. Service message: @@ -1982,9 +1982,9 @@ def _parse_message_thread_id( return message_thread_id # self.message_thread_id can be used for send_*.param.message_thread_id only if the - # thread is a forum topic. It does not work if the thread is a chain of replies to a - # message in a normal group. In that case, self.message_thread_id is just the message_id - # of the first message in the chain. + # thread is a forum topic (in supergroups or private chats). It does not work if the + # thread is a chain of replies to a message in a normal group. In that case, + # self.message_thread_id is just the message_id of the first message in the chain. if not self.is_topic_message: return None @@ -2077,6 +2077,55 @@ async def reply_text( suggested_post_parameters=suggested_post_parameters, ) + async def reply_text_draft( + self, + draft_id: int, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = None, + message_thread_id: ODVInput[int] = DEFAULT_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 = None, + ) -> bool: + """Shortcut for:: + + await bot.send_message_draft( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message_draft`. + + Note: + |reply_same_thread| + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + message_thread_id = self._parse_message_thread_id(self.chat_id, message_thread_id) + return await self.get_bot().send_message_draft( + chat_id=self.chat_id, + draft_id=draft_id, + text=text, + parse_mode=parse_mode, + entities=entities, + 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 reply_markdown( self, text: str, diff --git a/src/telegram/_user.py b/src/telegram/_user.py index 88597074416..4181d2f8cc8 100644 --- a/src/telegram/_user.py +++ b/src/telegram/_user.py @@ -112,6 +112,10 @@ class User(TelegramObject): Returned only in :meth:`telegram.Bot.get_me`. .. versionadded:: 21.5 + has_topics_enabled (:obj:`bool`, optional): :obj:`True`, if the bot has forum topic mode + enabled in private chats. Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this user or bot. @@ -143,6 +147,10 @@ class User(TelegramObject): Returned only in :meth:`telegram.Bot.get_me`. .. versionadded:: 21.5 + has_topics_enabled (:obj:`bool`): Optional. :obj:`True`, if the bot has forum topic mode + enabled in private chats. Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION .. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id` coincides with the :attr:`Chat.id` of the private chat with the user. This has been the @@ -156,6 +164,7 @@ class User(TelegramObject): "can_read_all_group_messages", "first_name", "has_main_web_app", + "has_topics_enabled", "id", "is_bot", "is_premium", @@ -180,6 +189,7 @@ def __init__( added_to_attachment_menu: bool | None = None, can_connect_to_business: bool | None = None, has_main_web_app: bool | None = None, + has_topics_enabled: bool | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -199,6 +209,7 @@ def __init__( self.added_to_attachment_menu: bool | None = added_to_attachment_menu self.can_connect_to_business: bool | None = can_connect_to_business self.has_main_web_app: bool | None = has_main_web_app + self.has_topics_enabled: bool | None = has_topics_enabled self._id_attrs = (self.id,) @@ -486,6 +497,49 @@ async def send_message( suggested_post_parameters=suggested_post_parameters, ) + async def send_message_draft( + self, + draft_id: int, + text: str, + message_thread_id: int | None = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = 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 = None, + ) -> bool: + """Shortcut for:: + + await bot.send_message_draft(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message_draft`. + + Note: + |user_chat_id_note| + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().send_message_draft( + chat_id=self.id, + draft_id=draft_id, + text=text, + message_thread_id=message_thread_id, + parse_mode=parse_mode, + entities=entities, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def delete_message( self, message_id: int, diff --git a/src/telegram/constants.py b/src/telegram/constants.py index 6680bbf930c..80c44eda10e 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -2010,6 +2010,8 @@ class MessageLimit(IntEnum): * :paramref:`~telegram.Bot.send_message.text` parameter of :meth:`telegram.Bot.send_message` * :paramref:`~telegram.Bot.edit_message_text.text` parameter of :meth:`telegram.Bot.edit_message_text` + * :paramref:`~telegram.Bot.send_message_draft.text` parameter of + :meth:`telegram.Bot.send_message_draft` """ CAPTION_LENGTH = 1024 """:obj:`int`: Maximum number of characters in a :obj:`str` passed as: @@ -2025,11 +2027,14 @@ class MessageLimit(IntEnum): """ # constants above this line are tested MIN_TEXT_LENGTH = 1 - """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the - :paramref:`~telegram.InputTextMessageContent.message_text` parameter of - :class:`telegram.InputTextMessageContent` and the - :paramref:`~telegram.Bot.edit_message_text.text` parameter of - :meth:`telegram.Bot.edit_message_text`. + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as: + + * :paramref:`~telegram.InputTextMessageContent.message_text` parameter of + :class:`telegram.InputTextMessageContent`. + * :paramref:`~telegram.Bot.edit_message_text.text` parameter of + :meth:`telegram.Bot.edit_message_text`. + * :paramref:`~telegram.Bot.send_message_draft.text` parameter of + :meth:`telegram.Bot.send_message_draft`. """ DEEP_LINK_LENGTH = 64 """:obj:`int`: Maximum number of characters for a deep link.""" diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index 3ab1b017c5e..b02868d420a 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -3134,6 +3134,36 @@ async def send_message( suggested_post_parameters=suggested_post_parameters, ) + async def send_message_draft( + self, + chat_id: int, + draft_id: int, + text: str, + message_thread_id: int | None = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = 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 = None, + rate_limit_args: RLARGS | None = None, + ) -> bool: + return await super().send_message_draft( + chat_id=chat_id, + draft_id=draft_id, + text=text, + message_thread_id=message_thread_id, + parse_mode=parse_mode, + entities=entities, + 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 send_photo( self, chat_id: int | str, @@ -5281,6 +5311,7 @@ async def approve_suggested_post( # updated camelCase aliases getMe = get_me sendMessage = send_message + sendMessageDraft = send_message_draft deleteMessage = delete_message deleteMessages = delete_messages forwardMessage = forward_message diff --git a/tests/test_bot.py b/tests/test_bot.py index 496be324dfe..51f96138988 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1477,6 +1477,61 @@ async def make_assertion(*args, **_): "SoSecretToken", ) + async def test_send_message_draft(self, offline_bot, monkeypatch): + entities = [ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 8), + ] + + async def make_assertions(*args, **kwargs): + params = kwargs.get("request_data").parameters + assert params.get("chat_id") == 123 + assert params.get("draft_id") == 1 + assert params.get("text") == "test test" + assert params.get("message_thread_id") == 9 + assert params.get("parse_mode") == "markdown" + assert params.get("entities") == [e.to_dict() for e in entities] + + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertions) + assert await offline_bot.send_message_draft( + chat_id=123, + draft_id=1, + text="test test", + message_thread_id=9, + parse_mode="markdown", + entities=entities, + ) + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, "Markdown"), ("HTML", "HTML"), (None, None)], + ) + async def test_send_message_draft_default_parse_mode( + self, default_bot, monkeypatch, passed_value, expected_value + ): + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("parse_mode") == expected_value + return True + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "chat_id": 123, + "draft_id": 1, + "text": "test test", + "message_thread_id": 9, + "entities": [ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 8), + ], + } + if passed_value is not DEFAULT_NONE: + kwargs["parse_mode"] = passed_value + + await default_bot.send_message_draft(**kwargs) + # TODO: Needs improvement. Need incoming shipping queries to test async def test_answer_shipping_query_ok(self, monkeypatch, offline_bot): # For now just test that our internals pass the correct data diff --git a/tests/test_chat.py b/tests/test_chat.py index 7ab0c2f1d0f..33bc65ee0e2 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -524,6 +524,21 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "send_message", make_assertion) assert await chat.send_message(text="test") + async def test_instance_method_send_message_draft(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["text"] == "test" + + assert check_shortcut_signature( + Chat.send_message_draft, Bot.send_message_draft, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.send_message_draft, chat.get_bot(), "send_message_draft" + ) + assert await check_defaults_handling(chat.send_message_draft, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_message_draft", make_assertion) + assert await chat.send_message_draft(draft_id=1, text="test") + async def test_instance_method_send_media_group(self, monkeypatch, chat): async def make_assertion(*_, **kwargs): return kwargs["chat_id"] == chat.id and kwargs["media"] == "test_media_group" diff --git a/tests/test_forum.py b/tests/test_forum.py index dc627eb8462..85f9b3a9db0 100644 --- a/tests/test_forum.py +++ b/tests/test_forum.py @@ -40,13 +40,20 @@ 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, + name=ForumTopicTestBase.TEST_TOPIC_NAME, + icon_color=ForumTopicTestBase.TEST_TOPIC_ICON_COLOR, icon_custom_emoji_id=emoji_id, + is_name_implicit=ForumTopicTestBase.is_name_implicit, ) -class TestForumTopicWithoutRequest: +class ForumTopicTestBase: + TEST_TOPIC_NAME = TEST_TOPIC_NAME + TEST_TOPIC_ICON_COLOR = TEST_TOPIC_ICON_COLOR + is_name_implicit = False + + +class TestForumTopicWithoutRequest(ForumTopicTestBase): def test_slot_behaviour(self, forum_topic_object): inst = forum_topic_object for attr in inst.__slots__: @@ -55,33 +62,37 @@ def test_slot_behaviour(self, forum_topic_object): 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_color == self.TEST_TOPIC_ICON_COLOR + assert forum_topic_object.name == self.TEST_TOPIC_NAME assert forum_topic_object.icon_custom_emoji_id == emoji_id + assert forum_topic_object.is_name_implicit == self.is_name_implicit def test_de_json(self, offline_bot, emoji_id, forum_group_id): json_dict = { "message_thread_id": forum_group_id, - "name": TEST_TOPIC_NAME, - "icon_color": TEST_TOPIC_ICON_COLOR, + "name": self.TEST_TOPIC_NAME, + "icon_color": self.TEST_TOPIC_ICON_COLOR, "icon_custom_emoji_id": emoji_id, + "is_name_implicit": self.is_name_implicit, } topic = ForumTopic.de_json(json_dict, offline_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_color == self.TEST_TOPIC_ICON_COLOR + assert topic.name == self.TEST_TOPIC_NAME assert topic.icon_custom_emoji_id == emoji_id + assert topic.is_name_implicit == self.is_name_implicit 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["name"] == self.TEST_TOPIC_NAME + assert topic_dict["icon_color"] == self.TEST_TOPIC_ICON_COLOR assert topic_dict["icon_custom_emoji_id"] == emoji_id + assert topic_dict["is_name_implicit"] == self.is_name_implicit def test_equality(self, emoji_id, forum_group_id): a = ForumTopic( @@ -289,10 +300,20 @@ async def test_close_reopen_hide_unhide_general_forum_topic(self, bot, forum_gro @pytest.fixture(scope="module") def topic_created(): - return ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) + return ForumTopicCreated( + name=ForumTopicCreatedTestBase.TEST_TOPIC_NAME, + icon_color=ForumTopicCreatedTestBase.TEST_TOPIC_ICON_COLOR, + is_name_implicit=ForumTopicCreatedTestBase.is_name_implicit, + ) + + +class ForumTopicCreatedTestBase: + TEST_TOPIC_NAME = TEST_TOPIC_NAME + TEST_TOPIC_ICON_COLOR = TEST_TOPIC_ICON_COLOR + is_name_implicit = False -class TestForumTopicCreatedWithoutRequest: +class TestForumTopicCreatedWithoutRequest(ForumTopicCreatedTestBase): def test_slot_behaviour(self, topic_created): for attr in topic_created.__slots__: assert getattr(topic_created, attr, "err") != "err", f"got extra slot '{attr}'" @@ -301,23 +322,30 @@ def test_slot_behaviour(self, topic_created): ) def test_expected_values(self, topic_created): - assert topic_created.icon_color == TEST_TOPIC_ICON_COLOR - assert topic_created.name == TEST_TOPIC_NAME + assert topic_created.icon_color == self.TEST_TOPIC_ICON_COLOR + assert topic_created.name == self.TEST_TOPIC_NAME + assert topic_created.is_name_implicit == self.is_name_implicit def test_de_json(self, offline_bot): - json_dict = {"icon_color": TEST_TOPIC_ICON_COLOR, "name": TEST_TOPIC_NAME} + json_dict = { + "icon_color": self.TEST_TOPIC_ICON_COLOR, + "name": self.TEST_TOPIC_NAME, + "is_name_implicit": self.is_name_implicit, + } action = ForumTopicCreated.de_json(json_dict, offline_bot) assert action.api_kwargs == {} - assert action.icon_color == TEST_TOPIC_ICON_COLOR - assert action.name == TEST_TOPIC_NAME + assert action.icon_color == self.TEST_TOPIC_ICON_COLOR + assert action.name == self.TEST_TOPIC_NAME + assert action.is_name_implicit == self.is_name_implicit 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 + assert action_dict["name"] == self.TEST_TOPIC_NAME + assert action_dict["icon_color"] == self.TEST_TOPIC_ICON_COLOR + assert action_dict["is_name_implicit"] == self.is_name_implicit def test_equality(self, emoji_id): a = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) diff --git a/tests/test_message.py b/tests/test_message.py index 3bafe22d54f..46d975942f4 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -735,7 +735,8 @@ async def extract_message_thread_id(*args, **kwargs): message_thread_id = await method(*args, message_thread_id=None) assert message_thread_id is None - if bot_method_name == "send_chat_action": + # These methods do not accept `do_quote` as passed below + if bot_method_name in ["send_chat_action", "send_message_draft"]: return message_thread_id = await method( @@ -1831,6 +1832,37 @@ async def make_assertion(*_, **kwargs): message, message.reply_html, "send_message", ["test"], monkeypatch ) + async def test_reply_text_draft(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + text = kwargs["text"] == "test" + return id_ and text + + assert check_shortcut_signature( + Message.reply_text_draft, + Bot.send_message_draft, + ["chat_id"], + [], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_text_draft, + message.get_bot(), + "send_message_draft", + skip_params=[""], + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling( + message.reply_text_draft, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_message_draft", make_assertion) + assert await message.reply_text_draft(draft_id=1, text="test") + + await self.check_thread_id_parsing( + message, message.reply_text_draft, "send_message_draft", [1, "test"], monkeypatch + ) + async def test_reply_media_group(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id diff --git a/tests/test_user.py b/tests/test_user.py index 490aa6052ec..fcafc6a42d2 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -44,6 +44,7 @@ def json_dict(): "added_to_attachment_menu": UserTestBase.added_to_attachment_menu, "can_connect_to_business": UserTestBase.can_connect_to_business, "has_main_web_app": UserTestBase.has_main_web_app, + "has_topics_enabled": UserTestBase.has_topics_enabled, } @@ -63,6 +64,7 @@ def user(bot): added_to_attachment_menu=UserTestBase.added_to_attachment_menu, can_connect_to_business=UserTestBase.can_connect_to_business, has_main_web_app=UserTestBase.has_main_web_app, + has_topics_enabled=UserTestBase.has_topics_enabled, ) user.set_bot(bot) user._unfreeze() @@ -83,6 +85,7 @@ class UserTestBase: added_to_attachment_menu = False can_connect_to_business = True has_main_web_app = False + has_topics_enabled = False class TestUserWithoutRequest(UserTestBase): @@ -108,6 +111,7 @@ def test_de_json(self, json_dict, offline_bot): assert user.added_to_attachment_menu == self.added_to_attachment_menu assert user.can_connect_to_business == self.can_connect_to_business assert user.has_main_web_app == self.has_main_web_app + assert user.has_topics_enabled == self.has_topics_enabled def test_to_dict(self, user): user_dict = user.to_dict() @@ -126,6 +130,7 @@ def test_to_dict(self, user): assert user_dict["added_to_attachment_menu"] == user.added_to_attachment_menu assert user_dict["can_connect_to_business"] == user.can_connect_to_business assert user_dict["has_main_web_app"] == user.has_main_web_app + assert user_dict["has_topics_enabled"] == user.has_topics_enabled def test_equality(self): a = User(self.id_, self.first_name, self.is_bot, self.last_name) @@ -231,6 +236,25 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(user.get_bot(), "send_message", make_assertion) assert await user.send_message("test") + async def test_instance_method_send_message_draft(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == user.id + and kwargs["draft_id"] == 123 + and kwargs["text"] == "test" + ) + + assert check_shortcut_signature( + User.send_message_draft, Bot.send_message_draft, ["chat_id"], [] + ) + assert await check_shortcut_call( + user.send_message_draft, user.get_bot(), "send_message_draft" + ) + assert await check_defaults_handling(user.send_message_draft, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_message_draft", make_assertion) + assert await user.send_message_draft(123, "test") + async def test_instance_method_send_photo(self, monkeypatch, user): async def make_assertion(*_, **kwargs): return kwargs["chat_id"] == user.id and kwargs["photo"] == "test_photo"