Repository: python-telegram-bot/python-telegram-bot

Issue:
[FEATURE] Reply functions should reply to the same thread by default even without quoting
### What kind of feature are you missing? Where do you notice a shortcoming of PTB?

When replying to messages from a group that has topics enabled using reply methods such as `Message.reply_html`, the reply is sent to the General topic by default. This is because the `Message.reply_html` method does not preserve the original `message_thread_id`.

Upon closer inspection, I see that this is tied to the `do_quote` parameter - if this parameter is set to `True` or `None` then the message is sent to the correct topic and the original message is quoted. If this parameter is set to `False` then the message is sent to the General topic and the original message is not quoted. There are legitimate use cases where you don't want to quote the original message (e.g. for brevity) but still want the reply to end up in the same thread.

I am using version 20.8 of the library.

### Describe the solution you'd like

In my opinion, the intuitive solution is for the reply to go to the same `message_thread_id` that the message came from by default. This is consistent with the behavior of replying to the same `chat_id` by default. Of course, if the caller wants to pass a different `message_thread_id`, that possibility is still available.

### Describe alternatives you've considered

One can update their code base to pass the `message_thread_id` parameter manually to the reply methods, but this seems overkill.

### Additional context

I can implement this myself and send a PR, but I was wondering if there was a specific design intent behind this decision.


Discussion:
Hi. This is not really tied to the `do_quote` parameter, this just sets the reply_to_message_id. The API however supports replying to messages without passing the thread id and it still is in the correct thread (partly because of backwards compatibility, partly in the way forums/topics work).

Anyhow, I fully agree with you implementing this. We did not add this because I missed it during the initial API update and since the default experience (quote is True in groups) does not break bots, this only comes up so sparingly that no one thought about setting this by default when a message object is available.

However, one can argue it changes the default behavior too much to not introduce this as a breaking change. I would argue it is fine, since it only ever can give users errors if they don't set this up correctly, and if we do honor the user passing manual thread ids there is no way to break existing setups. but that's up to the other maintainers to vote on.
> However, one can argue it changes the default behavior too much to not introduce this as a breaking change.

Since the `Message.reply_*` explicitly document which arguments they pass (

> Shortcut for:
>
> `await bot.send_message(update.effective_message.chat_id, *args, **kwargs)`

) and notably do not list `message_thread_id` here, one can certainly view this as "breaking documented behavior".
We do however have a sentence in our [stability policy](https://docs.python-telegram-bot.org/en/v20.8/stability_policy.html#what-doesn-t-this-policy-cover) saying that

> Introduction of […] changes of flavors of comparable behavior (e.g. the default for the HTTP protocol version being used) are not covered by this policy.

A rather broad interpretation of this sentence, this sentence would cover such a change IMO. Since I agree that replying to the same thread is the expected use case, I would be fine with using such an interpretation.
+1 😃 
yeah, the default behaviour should be changed 👍🏽 
Alrighz @andrei-livadariu if you want to go ahead with implementing it

Resolving patch:
diff --git a/telegram/_message.py b/telegram/_message.py
--- a/telegram/_message.py
+++ b/telegram/_message.py
@@ -824,6 +824,9 @@ class Message(MaybeInaccessibleMessage):
     .. |blockquote_no_md1_support| replace:: Since block quotation entities are not supported
        by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a
        :exc:`ValueError` when encountering a block quotation.
+
+    .. |reply_same_thread| replace:: If :paramref:`message_thread_id` is not provided,
+       this will reply to the same thread (topic) of the original message.
     """
 
     # fmt: on
@@ -1535,6 +1538,15 @@ async def _parse_quote_arguments(
 
         return chat_id, effective_reply_parameters
 
+    def _parse_message_thread_id(
+        self,
+        chat_id: Union[str, int],
+        message_thread_id: Optional[int] = None,
+    ) -> Optional[int]:
+        return message_thread_id or (
+            self.message_thread_id if chat_id in {self.chat_id, self.chat.username} else None
+        )
+
     async def reply_text(
         self,
         text: str,
@@ -1560,10 +1572,18 @@ async def reply_text(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_message(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_message(
+                 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`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -1581,6 +1601,7 @@ async def reply_text(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_message(
             chat_id=chat_id,
             text=text,
@@ -1627,6 +1648,7 @@ async def reply_markdown(
 
             await bot.send_message(
                 update.effective_message.chat_id,
+                message_thread_id=update.effective_message.message_thread_id,
                 parse_mode=ParseMode.MARKDOWN,
                 *args,
                 **kwargs,
@@ -1636,6 +1658,9 @@ async def reply_markdown(
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Note:
             :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by
             Telegram for backward compatibility. You should use :meth:`reply_markdown_v2` instead.
@@ -1656,6 +1681,7 @@ async def reply_markdown(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_message(
             chat_id=chat_id,
             text=text,
@@ -1702,6 +1728,7 @@ async def reply_markdown_v2(
 
             await bot.send_message(
                 update.effective_message.chat_id,
+                message_thread_id=update.effective_message.message_thread_id,
                 parse_mode=ParseMode.MARKDOWN_V2,
                 *args,
                 **kwargs,
@@ -1711,6 +1738,9 @@ async def reply_markdown_v2(
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -1727,6 +1757,7 @@ async def reply_markdown_v2(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_message(
             chat_id=chat_id,
             text=text,
@@ -1773,6 +1804,7 @@ async def reply_html(
 
             await bot.send_message(
                 update.effective_message.chat_id,
+                message_thread_id=update.effective_message.message_thread_id,
                 parse_mode=ParseMode.HTML,
                 *args,
                 **kwargs,
@@ -1782,6 +1814,9 @@ async def reply_html(
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -1798,6 +1833,7 @@ async def reply_html(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_message(
             chat_id=chat_id,
             text=text,
@@ -1843,10 +1879,18 @@ async def reply_media_group(
     ) -> Tuple["Message", ...]:
         """Shortcut for::
 
-             await bot.send_media_group(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_media_group(
+                 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_media_group`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -1866,6 +1910,7 @@ async def reply_media_group(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_media_group(
             chat_id=chat_id,
             media=media,
@@ -1910,10 +1955,18 @@ async def reply_photo(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_photo(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_photo(
+                 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_photo`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -1931,6 +1984,7 @@ async def reply_photo(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_photo(
             chat_id=chat_id,
             photo=photo,
@@ -1981,10 +2035,18 @@ async def reply_audio(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_audio(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_audio(
+                 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_audio`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2002,6 +2064,7 @@ async def reply_audio(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_audio(
             chat_id=chat_id,
             audio=audio,
@@ -2053,10 +2116,18 @@ async def reply_document(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_document(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_document(
+                 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_document`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2074,6 +2145,7 @@ async def reply_document(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_document(
             chat_id=chat_id,
             document=document,
@@ -2126,10 +2198,18 @@ async def reply_animation(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_animation(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_animation(
+                 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_animation`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2147,6 +2227,7 @@ async def reply_animation(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_animation(
             chat_id=chat_id,
             animation=animation,
@@ -2194,10 +2275,18 @@ async def reply_sticker(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_sticker(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_sticker(
+                 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_sticker`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2215,6 +2304,7 @@ async def reply_sticker(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_sticker(
             chat_id=chat_id,
             sticker=sticker,
@@ -2263,10 +2353,18 @@ async def reply_video(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_video(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_video(
+                 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_video`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2284,6 +2382,7 @@ async def reply_video(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_video(
             chat_id=chat_id,
             video=video,
@@ -2335,10 +2434,18 @@ async def reply_video_note(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_video_note(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_video_note(
+                 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_video_note`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2356,6 +2463,7 @@ async def reply_video_note(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_video_note(
             chat_id=chat_id,
             video_note=video_note,
@@ -2402,10 +2510,18 @@ async def reply_voice(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_voice(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_voice(
+                 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_voice`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2423,6 +2539,7 @@ async def reply_voice(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_voice(
             chat_id=chat_id,
             voice=voice,
@@ -2471,10 +2588,18 @@ async def reply_location(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_location(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_location(
+                 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_location`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2492,6 +2617,7 @@ async def reply_location(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_location(
             chat_id=chat_id,
             latitude=latitude,
@@ -2543,10 +2669,18 @@ async def reply_venue(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_venue(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_venue(
+                 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_venue`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2564,6 +2698,7 @@ async def reply_venue(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_venue(
             chat_id=chat_id,
             latitude=latitude,
@@ -2613,10 +2748,18 @@ async def reply_contact(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_contact(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_contact(
+                 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_contact`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2634,6 +2777,7 @@ async def reply_contact(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_contact(
             chat_id=chat_id,
             phone_number=phone_number,
@@ -2686,10 +2830,18 @@ async def reply_poll(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_poll(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_poll(
+                 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_poll`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2707,6 +2859,7 @@ async def reply_poll(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_poll(
             chat_id=chat_id,
             question=question,
@@ -2755,10 +2908,18 @@ async def reply_dice(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_dice(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_dice(
+                 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_dice`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2776,6 +2937,7 @@ async def reply_dice(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_dice(
             chat_id=chat_id,
             disable_notification=disable_notification,
@@ -2805,10 +2967,18 @@ async def reply_chat_action(
     ) -> bool:
         """Shortcut for::
 
-             await bot.send_chat_action(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_chat_action(
+                 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_chat_action`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         .. versionadded:: 13.2
 
         Returns:
@@ -2817,7 +2987,7 @@ async def reply_chat_action(
         """
         return await self.get_bot().send_chat_action(
             chat_id=self.chat_id,
-            message_thread_id=message_thread_id,
+            message_thread_id=self._parse_message_thread_id(self.chat_id, message_thread_id),
             action=action,
             read_timeout=read_timeout,
             write_timeout=write_timeout,
@@ -2847,10 +3017,18 @@ async def reply_game(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_game(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_game(
+                 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_game`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -2870,6 +3048,7 @@ async def reply_game(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_game(
             chat_id=chat_id,  # type: ignore[arg-type]
             game_short_name=game_short_name,
@@ -2927,10 +3106,18 @@ async def reply_invoice(
     ) -> "Message":
         """Shortcut for::
 
-             await bot.send_invoice(update.effective_message.chat_id, *args, **kwargs)
+             await bot.send_invoice(
+                 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_invoice`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Warning:
             As of API 5.2 :paramref:`start_parameter <telegram.Bot.send_invoice.start_parameter>`
             is an optional argument and therefore the
@@ -2960,6 +3147,7 @@ async def reply_invoice(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().send_invoice(
             chat_id=chat_id,
             title=title,
@@ -3130,6 +3318,7 @@ async def reply_copy(
 
              await bot.copy_message(
                  chat_id=message.chat.id,
+                 message_thread_id=update.effective_message.message_thread_id,
                  message_id=message_id,
                  *args,
                  **kwargs
@@ -3137,6 +3326,9 @@ async def reply_copy(
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`.
 
+        .. versionchanged:: NEXT.VERSION
+                |reply_same_thread|
+
         Keyword Args:
             quote (:obj:`bool`, optional): |reply_quote|
 
@@ -3155,6 +3347,7 @@ async def reply_copy(
         chat_id, effective_reply_parameters = await self._parse_quote_arguments(
             do_quote, quote, reply_to_message_id, reply_parameters
         )
+        message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
         return await self.get_bot().copy_message(
             chat_id=chat_id,
             from_chat_id=from_chat_id,


Test patch:
diff --git a/tests/test_message.py b/tests/test_message.py
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -482,6 +482,48 @@ async def make_assertion(*args, **kwargs):
         if reply_parameters is None or reply_parameters.message_id != 42:
             pytest.fail(f"reply_parameters is {reply_parameters} but should be 42")
 
+    @staticmethod
+    async def check_thread_id_parsing(
+        message: Message, method, bot_method_name: str, args, monkeypatch
+    ):
+        """Used in testing reply_* below. Makes sure that meassage_thread_id is parsed
+        correctly."""
+
+        async def extract_message_thread_id(*args, **kwargs):
+            return kwargs.get("message_thread_id")
+
+        monkeypatch.setattr(message.get_bot(), bot_method_name, extract_message_thread_id)
+
+        message.message_thread_id = None
+        message_thread_id = await method(*args)
+        assert message_thread_id is None
+
+        message.message_thread_id = 99
+        message_thread_id = await method(*args)
+        assert message_thread_id == 99
+
+        message_thread_id = await method(*args, message_thread_id=50)
+        assert message_thread_id == 50
+
+        if bot_method_name == "send_chat_action":
+            return
+
+        message_thread_id = await method(
+            *args,
+            do_quote=message.build_reply_arguments(
+                target_chat_id=123,
+            ),
+        )
+        assert message_thread_id is None
+
+        message_thread_id = await method(
+            *args,
+            do_quote=message.build_reply_arguments(
+                target_chat_id=message.chat_id,
+            ),
+        )
+        assert message_thread_id == message.message_thread_id
+
     def test_slot_behaviour(self):
         message = Message(
             message_id=TestMessageBase.id_,
@@ -1361,6 +1403,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_text, "send_message", ["test"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_text, "send_message", ["test"], monkeypatch
+        )
+
     async def test_reply_markdown(self, monkeypatch, message):
         test_md_string = (
             r"Test for <*bold*, _ita_\__lic_, `code`, "
@@ -1395,6 +1441,10 @@ async def make_assertion(*_, **kwargs):
         monkeypatch.setattr(message.get_bot(), "send_message", make_assertion)
         assert await message.reply_markdown(self.test_message.text_markdown)
 
+        await self.check_thread_id_parsing(
+            message, message.reply_markdown, "send_message", ["test"], monkeypatch
+        )
+
     async def test_reply_markdown_v2(self, monkeypatch, message):
         test_md_string = (
             r"__Test__ for <*bold*, _ita\_lic_, `\\\`code`, "
@@ -1436,6 +1486,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_markdown_v2, "send_message", [test_md_string], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_markdown_v2, "send_message", ["test"], monkeypatch
+        )
+
     async def test_reply_html(self, monkeypatch, message):
         test_html_string = (
             "<u>Test</u> for &lt;<b>bold</b>, <i>ita_lic</i>, "
@@ -1479,6 +1533,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_html, "send_message", [test_html_string], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_html, "send_message", ["test"], monkeypatch
+        )
+
     async def test_reply_media_group(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1509,6 +1567,14 @@ async def make_assertion(*_, **kwargs):
             monkeypatch,
         )
 
+        await self.check_thread_id_parsing(
+            message,
+            message.reply_media_group,
+            "send_media_group",
+            ["reply_media_group"],
+            monkeypatch,
+        )
+
     async def test_reply_photo(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1535,6 +1601,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_photo, "send_photo", ["test_photo"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_photo, "send_photo", ["test_photo"], monkeypatch
+        )
+
     async def test_reply_audio(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1561,6 +1631,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_audio, "send_audio", ["test_audio"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_audio, "send_audio", ["test_audio"], monkeypatch
+        )
+
     async def test_reply_document(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1587,6 +1661,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_document, "send_document", ["test_document"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_document, "send_document", ["test_document"], monkeypatch
+        )
+
     async def test_reply_animation(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1613,6 +1691,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_animation, "send_animation", ["test_animation"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_animation, "send_animation", ["test_animation"], monkeypatch
+        )
+
     async def test_reply_sticker(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1639,6 +1721,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_sticker, "send_sticker", ["test_sticker"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_sticker, "send_sticker", ["test_sticker"], monkeypatch
+        )
+
     async def test_reply_video(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1665,6 +1751,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_video, "send_video", ["test_video"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_video, "send_video", ["test_video"], monkeypatch
+        )
+
     async def test_reply_video_note(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1691,6 +1781,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_video_note, "send_video_note", ["test_video_note"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_video_note, "send_video_note", ["test_video_note"], monkeypatch
+        )
+
     async def test_reply_voice(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1717,6 +1811,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_voice, "send_voice", ["test_voice"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_voice, "send_voice", ["test_voice"], monkeypatch
+        )
+
     async def test_reply_location(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1743,6 +1841,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_location, "send_location", ["test_location"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_location, "send_location", ["test_location"], monkeypatch
+        )
+
     async def test_reply_venue(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1769,6 +1871,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_venue, "send_venue", ["test_venue"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_venue, "send_venue", ["test_venue"], monkeypatch
+        )
+
     async def test_reply_contact(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1795,6 +1901,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_contact, "send_contact", ["test_contact"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_contact, "send_contact", ["test_contact"], monkeypatch
+        )
+
     async def test_reply_poll(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1819,6 +1929,10 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_poll, "send_poll", ["test_poll", ["1", "2", "3"]], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_poll, "send_poll", ["test_poll", ["1", "2", "3"]], monkeypatch
+        )
+
     async def test_reply_dice(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1846,6 +1960,10 @@ async def make_assertion(*_, **kwargs):
             monkeypatch,
         )
 
+        await self.check_thread_id_parsing(
+            message, message.reply_dice, "send_dice", [], monkeypatch
+        )
+
     async def test_reply_action(self, monkeypatch, message: Message):
         async def make_assertion(*_, **kwargs):
             id_ = kwargs["chat_id"] == message.chat_id
@@ -1863,6 +1981,14 @@ async def make_assertion(*_, **kwargs):
         monkeypatch.setattr(message.get_bot(), "send_chat_action", make_assertion)
         assert await message.reply_chat_action(action=ChatAction.TYPING)
 
+        await self.check_thread_id_parsing(
+            message,
+            message.reply_chat_action,
+            "send_chat_action",
+            [ChatAction.TYPING],
+            monkeypatch,
+        )
+
     async def test_reply_game(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             return (
@@ -1886,6 +2012,14 @@ async def make_assertion(*_, **kwargs):
             message, message.reply_game, "send_game", ["test_game"], monkeypatch
         )
 
+        await self.check_thread_id_parsing(
+            message,
+            message.reply_game,
+            "send_game",
+            ["test_game"],
+            monkeypatch,
+        )
+
     async def test_reply_invoice(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             title = kwargs["title"] == "title"
@@ -1928,6 +2062,14 @@ async def make_assertion(*_, **kwargs):
             monkeypatch,
         )
 
+        await self.check_thread_id_parsing(
+            message,
+            message.reply_invoice,
+            "send_invoice",
+            ["title", "description", "payload", "provider_token", "currency", "prices"],
+            monkeypatch,
+        )
+
     @pytest.mark.parametrize(("disable_notification", "protected"), [(False, True), (True, False)])
     async def test_forward(self, monkeypatch, message, disable_notification, protected):
         async def make_assertion(*_, **kwargs):
@@ -2042,6 +2184,14 @@ async def make_assertion(*_, **kwargs):
             monkeypatch,
         )
 
+        await self.check_thread_id_parsing(
+            message,
+            message.reply_copy,
+            "copy_message",
+            [123456, 456789],
+            monkeypatch,
+        )
+
     async def test_edit_text(self, monkeypatch, message):
         async def make_assertion(*_, **kwargs):
             chat_id = kwargs["chat_id"] == message.chat_id