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

Issue:
[BUG] `Message.reply_text` leads to `Message thread not found`
### Steps to Reproduce

It works when I use it directly in a group, but when I quote a message the send command doesn't work.

### Expected behaviour

![截图_20240414143721](https://github.com/python-telegram-bot/python-telegram-bot/assets/19776350/c72a3d1c-7c02-4a30-8e0f-d040dd31a1dd)


### Actual behaviour

async def get_hitokoto(update: Update, context: ContextTypes) -> None:
        url = "https://v1.hitokoto.cn/"
        await update.message.reply_text(f"{url }", parse_mode="Markdown",message_thread_id=None)

### Operating System

docker 

### Version of Python, python-telegram-bot & dependencies

```shell
python-telegram-bot==21.1
```


### Relevant log output

```python
2024-04-14 06:31:43,530 - telegram.ext.Application - ERROR - No error handlers are registered, logging exception.
Traceback (most recent call last):
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/ext/_application.py", line 1277, in process_update
    await coroutine
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/ext/_handlers/basehandler.py", line 157, in handle_update
    return await self.callback(update, context)
  File "/workspaces/tg/mypackage/tgbot.py", line 745, in get_hitokoto
    await update.message.reply_text(f"{formatted_output}", parse_mode="Markdown",message_thread_id=None)
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/_message.py", line 1652, in reply_text
    return await self.get_bot().send_message(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/ext/_extbot.py", line 2846, in send_message
    return await super().send_message(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/_bot.py", line 1012, in send_message
    return await self._send_message(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/ext/_extbot.py", line 587, in _send_message
    result = await super()._send_message(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/_bot.py", line 731, in _send_message
    result = await self._post(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/_bot.py", line 615, in _post
    return await self._do_post(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/ext/_extbot.py", line 369, in _do_post
    return await self.rate_limiter.process_request(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/ext/_aioratelimiter.py", line 245, in process_request
    return await self._run_request(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/ext/_aioratelimiter.py", line 203, in _run_request
    return await callback(*args, **kwargs)
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/_bot.py", line 644, in _do_post
    result = await request.post(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/request/_baserequest.py", line 200, in post
    result = await self._request_wrapper(
  File "/home/codespace/.python/current/lib/python3.10/site-packages/telegram/request/_baserequest.py", line 379, in _request_wrapper
    raise BadRequest(message)
telegram.error.BadRequest: Message thread not found
```


### Additional Context

_No response_


Discussion:
Hi. Thanks for reaching out. I tried to reproduce the behavior, but don't see an issue. What I've tried is the following.

Use this mwe:

<details>

```python
import logging

from telegram import Update
from telegram.ext import (
    ApplicationBuilder,
    ContextTypes,
    MessageHandler,
    filters,
    Defaults,
)

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.WARNING
)
# set higher logging level for httpx to avoid all GET and POST requests being logged
logging.getLogger("telegram.Bot").setLevel(logging.DEBUG)
logging.getLogger("telegram.ext.ExtBot").setLevel(logging.DEBUG)


async def callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    chat_type = update.message.chat.type
    has_topics = bool(update.message.message_thread_id)
    await update.message.reply_text(
        f"Test reply for chat of type: {chat_type} with topics: {has_topics}",
        message_thread_id=None,
    )


if __name__ == "__main__":

    application = (
        ApplicationBuilder()
        .token("TOKEN")
        # I've tried with and without defaults.
        .defaults(Defaults(do_quote=True))
        .build()
    )
    application.add_handler(MessageHandler(filters.TEXT, callback))

    application.run_polling(allowed_updates=Update.ALL_TYPES)
```
</details>

Then I've sent messages to the bot in the following chats:

1. private chat
2. group chat
3. general topic of a forum group
4. special topic of a forum group

Everything worked as expected for me.

Can you please provide an [MWE](../wiki/MWE) along with instructions on how to trigger the exception? Please mention group type, privacy settings of the bot, and any other relevant parameters.
```
import logging

from telegram import Update
from telegram.ext import (
    ApplicationBuilder,
    ContextTypes,
    MessageHandler,
    filters,
    Defaults,CommandHandler,
)

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.WARNING
)
# set higher logging level for httpx to avoid all GET and POST requests being logged
logging.getLogger("telegram.Bot").setLevel(logging.DEBUG)
logging.getLogger("telegram.ext.ExtBot").setLevel(logging.DEBUG)


async def callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    chat_type = update.message.chat.type
    has_topics = bool(update.message.message_thread_id)
    await update.message.reply_text(
        f"Test reply for chat of type: {chat_type} with topics: {has_topics}",
        message_thread_id=None,
    )


if __name__ == "__main__":

    application = (
        ApplicationBuilder()
        .token("token")
        # I've tried with and without defaults.
        .defaults(Defaults(do_quote=True))
        .build()
    )
    #application.add_handler(MessageHandler(filters.TEXT, callback))
    application.add_handler(CommandHandler("ja", callback))
    application.run_polling(allowed_updates=Update.ALL_TYPES)

```
Like this, when you use the ja command in this group, it works normally, but when you use ja to reply to a message in the group, an error message is reported.
Can you see the picture? If you use the command like the picture, you will get an error. like this
![IMG_1361](https://github.com/python-telegram-bot/python-telegram-bot/assets/19776350/f42da2eb-4e86-4308-be52-7dcd0151a9ad)
 
![IMG_1362](https://github.com/python-telegram-bot/python-telegram-bot/assets/19776350/c3d17fd0-09f3-4923-bb00-c61b2c7a1d27)

Thanks for elaborating (though in the future, please send 1 comment instead of several and paste monospaced text instead of posting screenshots 😉 )

I think I found the issued

`Message.message_thread_id` can signify *either* the id of a forum topic. Or it can be the message_id of a message this message is a reply to. We have no way of knownig which one it is.
However, the parameter `message_thread_id` of `send_message` accepts only ids of forum topics. Passing a message id is not allowed (instead `reply_parameters` must be used).

My first idea was that we can fix that, by checking if `Message.message_thread_id == Message.reply_to_message.message_id`. however, this doesn't work either because:

>>> Message 1

>> Message 2

> Message 3

Here, Message3 will have `message_thread_id==1` even though `reply_to_message.message_id==2`. I.e., 3 is a reply to 2, but since 2 is a reply to 1, the thread is the one of message 1.

TBH at first glance, I don't think how we can make this work reliably as long as we don't know if `message_thread_id` originates from a forum topic or from a message_id.

---

As a side note: if we get this to work, we should make `reply_*.param.message_thread_id` default to `DEFAULT_NONE` such that passing `None` actually removes the parameter.

Resolving patch:
diff --git a/telegram/_message.py b/telegram/_message.py
--- a/telegram/_message.py
+++ b/telegram/_message.py
@@ -1462,7 +1462,7 @@ def build_reply_arguments(
         quote_index: Optional[int] = None,
         target_chat_id: Optional[Union[int, str]] = None,
         allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
     ) -> _ReplyKwargs:
         """
         Builds a dictionary with the keys ``chat_id`` and ``reply_parameters``. This dictionary can
@@ -1587,11 +1587,22 @@ async def _parse_quote_arguments(
     def _parse_message_thread_id(
         self,
         chat_id: Union[str, int],
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
     ) -> Optional[int]:
-        return message_thread_id or (
-            self.message_thread_id if chat_id in {self.chat_id, self.chat.username} else None
-        )
+        # values set by user have the highest priority
+        if not isinstance(message_thread_id, DefaultValue):
+            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.
+        if not self.is_topic_message:
+            return None
+
+        # Setting message_thread_id=self.message_thread_id only makes sense if we're replying in
+        # the same chat.
+        return self.message_thread_id if chat_id in {self.chat_id, self.chat.username} else None
 
     async def reply_text(
         self,
@@ -1601,7 +1612,7 @@ async def reply_text(
         reply_markup: Optional[ReplyMarkup] = None,
         entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
@@ -1677,7 +1688,7 @@ async def reply_markdown(
         reply_markup: Optional[ReplyMarkup] = None,
         entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
@@ -1759,7 +1770,7 @@ async def reply_markdown_v2(
         reply_markup: Optional[ReplyMarkup] = None,
         entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
@@ -1837,7 +1848,7 @@ async def reply_html(
         reply_markup: Optional[ReplyMarkup] = None,
         entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
@@ -1915,7 +1926,7 @@ async def reply_media_group(
         ],
         disable_notification: ODVInput[bool] = DEFAULT_NONE,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,
@@ -1994,7 +2005,7 @@ async def reply_photo(
         parse_mode: ODVInput[str] = DEFAULT_NONE,
         caption_entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         has_spoiler: Optional[bool] = None,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
@@ -2076,7 +2087,7 @@ async def reply_audio(
         parse_mode: ODVInput[str] = DEFAULT_NONE,
         caption_entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         thumbnail: Optional[FileInput] = None,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
@@ -2159,7 +2170,7 @@ async def reply_document(
         disable_content_type_detection: Optional[bool] = None,
         caption_entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         thumbnail: Optional[FileInput] = None,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
@@ -2242,7 +2253,7 @@ async def reply_animation(
         reply_markup: Optional[ReplyMarkup] = None,
         caption_entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         has_spoiler: Optional[bool] = None,
         thumbnail: Optional[FileInput] = None,
         reply_parameters: Optional["ReplyParameters"] = None,
@@ -2323,7 +2334,7 @@ async def reply_sticker(
         disable_notification: ODVInput[bool] = DEFAULT_NONE,
         reply_markup: Optional[ReplyMarkup] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         emoji: Optional[str] = None,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
@@ -2401,7 +2412,7 @@ async def reply_video(
         supports_streaming: Optional[bool] = None,
         caption_entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         has_spoiler: Optional[bool] = None,
         thumbnail: Optional[FileInput] = None,
         reply_parameters: Optional["ReplyParameters"] = None,
@@ -2485,7 +2496,7 @@ async def reply_video_note(
         disable_notification: ODVInput[bool] = DEFAULT_NONE,
         reply_markup: Optional[ReplyMarkup] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         thumbnail: Optional[FileInput] = None,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
@@ -2564,7 +2575,7 @@ async def reply_voice(
         parse_mode: ODVInput[str] = DEFAULT_NONE,
         caption_entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,
@@ -2644,7 +2655,7 @@ async def reply_location(
         heading: Optional[int] = None,
         proximity_alert_radius: Optional[int] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,
@@ -2727,7 +2738,7 @@ async def reply_venue(
         google_place_id: Optional[str] = None,
         google_place_type: Optional[str] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,
@@ -2808,7 +2819,7 @@ async def reply_contact(
         reply_markup: Optional[ReplyMarkup] = None,
         vcard: Optional[str] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,
@@ -2893,7 +2904,7 @@ async def reply_poll(
         close_date: Optional[Union[int, datetime.datetime]] = None,
         explanation_entities: Optional[Sequence["MessageEntity"]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,
@@ -2973,7 +2984,7 @@ async def reply_dice(
         reply_markup: Optional[ReplyMarkup] = None,
         emoji: Optional[str] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,
@@ -3039,7 +3050,7 @@ async def reply_dice(
     async def reply_chat_action(
         self,
         action: str,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         *,
         read_timeout: ODVInput[float] = DEFAULT_NONE,
         write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3086,7 +3097,7 @@ async def reply_game(
         disable_notification: ODVInput[bool] = DEFAULT_NONE,
         reply_markup: Optional["InlineKeyboardMarkup"] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,
@@ -3177,7 +3188,7 @@ async def reply_invoice(
         max_tip_amount: Optional[int] = None,
         suggested_tip_amounts: Optional[Sequence[int]] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,
@@ -3387,7 +3398,7 @@ async def reply_copy(
         disable_notification: ODVInput[bool] = DEFAULT_NONE,
         reply_markup: Optional[ReplyMarkup] = None,
         protect_content: ODVInput[bool] = DEFAULT_NONE,
-        message_thread_id: Optional[int] = None,
+        message_thread_id: ODVInput[int] = DEFAULT_NONE,
         reply_parameters: Optional["ReplyParameters"] = None,
         *,
         reply_to_message_id: Optional[int] = None,


Test patch:
diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py
--- a/tests/auxil/bot_method_checks.py
+++ b/tests/auxil/bot_method_checks.py
@@ -21,7 +21,7 @@
 import functools
 import inspect
 import re
-from typing import Any, Callable, Dict, Iterable, List, Optional
+from typing import Any, Callable, Collection, Dict, Iterable, List, Optional, Tuple
 
 import pytest
 
@@ -59,6 +59,7 @@ def check_shortcut_signature(
     bot_method: Callable,
     shortcut_kwargs: List[str],
     additional_kwargs: List[str],
+    annotation_overrides: Optional[Dict[str, Tuple[Any, Any]]] = None,
 ) -> bool:
     """
     Checks that the signature of a shortcut matches the signature of the underlying bot method.
@@ -69,10 +70,14 @@ def check_shortcut_signature(
         shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id``
         additional_kwargs: Additional kwargs of the shortcut that the bot method doesn't have, e.g.
             ``quote``.
+        annotation_overrides: A dictionary of exceptions for the annotation comparison. The key is
+            the name of the argument, the value is a tuple of the expected annotation and
+            the default value. E.g. ``{'parse_mode': (str, 'None')}``.
 
     Returns:
         :obj:`bool`: Whether or not the signature matches.
     """
+    annotation_overrides = annotation_overrides or {}
 
     def resolve_class(class_name: str) -> Optional[type]:
         """Attempts to resolve a PTB class (telegram module only) from a ForwardRef.
@@ -117,6 +122,14 @@ def resolve_class(class_name: str) -> Optional[type]:
         if shortcut_sig.parameters[kwarg].kind != expected_kind:
             raise Exception(f"Argument {kwarg} must be of kind {expected_kind}.")
 
+        if kwarg in annotation_overrides:
+            if shortcut_sig.parameters[kwarg].annotation != annotation_overrides[kwarg][0]:
+                raise Exception(
+                    f"For argument {kwarg} I expected {annotation_overrides[kwarg]}, "
+                    f"but got {shortcut_sig.parameters[kwarg].annotation}"
+                )
+            continue
+
         if bot_sig.parameters[kwarg].annotation != shortcut_sig.parameters[kwarg].annotation:
             if FORWARD_REF_PATTERN.search(str(shortcut_sig.parameters[kwarg])):
                 # If a shortcut signature contains a ForwardRef, the simple comparison of
@@ -155,6 +168,13 @@ def resolve_class(class_name: str) -> Optional[type]:
     bot_method_sig = inspect.signature(bot_method)
     shortcut_sig = inspect.signature(shortcut)
     for arg in expected_args:
+        if arg in annotation_overrides:
+            if shortcut_sig.parameters[arg].default == annotation_overrides[arg][1]:
+                continue
+            raise Exception(
+                f"For argument {arg} I expected default {annotation_overrides[arg][1]}, "
+                f"but got {shortcut_sig.parameters[arg].default}"
+            )
         if not shortcut_sig.parameters[arg].default == bot_method_sig.parameters[arg].default:
             raise Exception(
                 f"Default for argument {arg} does not match the default of the Bot method."
@@ -525,6 +545,7 @@ async def check_defaults_handling(
     method: Callable,
     bot: Bot,
     return_value=None,
+    no_default_kwargs: Collection[str] = frozenset(),
 ) -> bool:
     """
     Checks that tg.ext.Defaults are handled correctly.
@@ -536,6 +557,8 @@ async def check_defaults_handling(
         return_value: Optional. The return value of Bot._post that the method expects. Defaults to
             None. get_file is automatically handled. If this is a `TelegramObject`, Bot._post will
             return the `to_dict` representation of it.
+        no_default_kwargs: Optional. A collection of keyword arguments that should not have default
+            values. Defaults to an empty frozenset.
 
     """
     raw_bot = not isinstance(bot, ExtBot)
@@ -545,7 +568,9 @@ async def check_defaults_handling(
     kwargs_need_default = {
         kwarg
         for kwarg, value in shortcut_signature.parameters.items()
-        if isinstance(value.default, DefaultValue) and not kwarg.endswith("_timeout")
+        if isinstance(value.default, DefaultValue)
+        and not kwarg.endswith("_timeout")
+        and kwarg not in no_default_kwargs
     }
     # We tested this for a long time, but Bot API 7.0 deprecated it in favor of
     # reply_parameters. In the transition phase, both exist in a mutually exclusive
diff --git a/tests/test_message.py b/tests/test_message.py
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -69,6 +69,8 @@
     WebAppData,
 )
 from telegram._utils.datetime import UTC
+from telegram._utils.defaultvalue import DEFAULT_NONE
+from telegram._utils.types import ODVInput
 from telegram.constants import ChatAction, ParseMode
 from telegram.ext import Defaults
 from telegram.warnings import PTBDeprecationWarning
@@ -502,35 +504,42 @@ async def extract_message_thread_id(*args, **kwargs):
 
         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
+        for is_topic_message in (True, False):
+            message.is_topic_message = is_topic_message
 
-        message.message_thread_id = 99
-        message_thread_id = await method(*args)
-        assert message_thread_id == 99
+            message.message_thread_id = None
+            message_thread_id = await method(*args)
+            assert message_thread_id is None
 
-        message_thread_id = await method(*args, message_thread_id=50)
-        assert message_thread_id == 50
+            message.message_thread_id = 99
+            message_thread_id = await method(*args)
+            assert message_thread_id == (99 if is_topic_message else None)
 
-        if bot_method_name == "send_chat_action":
-            return
+            message_thread_id = await method(*args, message_thread_id=50)
+            assert message_thread_id == 50
 
-        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, message_thread_id=None)
+            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
+            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
+
+            for target_chat_id in (message.chat_id, message.chat.username):
+                message_thread_id = await method(
+                    *args,
+                    do_quote=message.build_reply_arguments(
+                        target_chat_id=target_chat_id,
+                    ),
+                )
+            assert message_thread_id == (message.message_thread_id if is_topic_message else None)
 
     def test_slot_behaviour(self):
         message = Message(
@@ -1396,6 +1405,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_message,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_text,
@@ -1404,7 +1414,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_text, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_message", make_assertion)
         assert await message.reply_text("test")
@@ -1435,6 +1447,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_message,
             ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_text,
@@ -1443,7 +1456,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_text, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         text_markdown = self.test_message.text_markdown
         assert text_markdown == test_md_string
@@ -1478,6 +1493,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_message,
             ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_text,
@@ -1486,7 +1502,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_text, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         text_markdown = self.test_message_v2.text_markdown_v2
         assert text_markdown == test_md_string
@@ -1526,6 +1544,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_message,
             ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_text,
@@ -1534,7 +1553,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_text, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         text_html = self.test_message_v2.text_html
         assert text_html == test_html_string
@@ -1560,6 +1581,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_media_group,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_media_group,
@@ -1568,7 +1590,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_media_group, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_media_group, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_media_group", make_assertion)
         assert await message.reply_media_group(media="reply_media_group")
@@ -1599,6 +1623,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_photo,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_photo,
@@ -1607,7 +1632,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_photo, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_photo, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_photo", make_assertion)
         assert await message.reply_photo(photo="test_photo")
@@ -1630,6 +1657,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_audio,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_audio,
@@ -1638,7 +1666,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_audio, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_audio, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_audio", make_assertion)
         assert await message.reply_audio(audio="test_audio")
@@ -1661,6 +1691,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_document,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_document,
@@ -1669,7 +1700,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_document, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_document, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_document", make_assertion)
         assert await message.reply_document(document="test_document")
@@ -1692,6 +1725,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_animation,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_animation,
@@ -1700,7 +1734,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_animation, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_animation, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_animation", make_assertion)
         assert await message.reply_animation(animation="test_animation")
@@ -1723,6 +1759,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_sticker,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_sticker,
@@ -1731,7 +1768,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_sticker, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_sticker, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_sticker", make_assertion)
         assert await message.reply_sticker(sticker="test_sticker")
@@ -1754,6 +1793,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_video,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_video,
@@ -1762,7 +1802,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_video, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_video, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_video", make_assertion)
         assert await message.reply_video(video="test_video")
@@ -1785,6 +1827,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_video_note,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_video_note,
@@ -1793,7 +1836,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_video_note, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_video_note, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_video_note", make_assertion)
         assert await message.reply_video_note(video_note="test_video_note")
@@ -1816,6 +1861,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_voice,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_voice,
@@ -1824,7 +1870,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_voice, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_voice, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_voice", make_assertion)
         assert await message.reply_voice(voice="test_voice")
@@ -1847,6 +1895,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_location,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_location,
@@ -1855,7 +1904,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_location, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_location, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_location", make_assertion)
         assert await message.reply_location(location="test_location")
@@ -1878,6 +1929,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_venue,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_venue,
@@ -1886,7 +1938,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_venue, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_venue, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_venue", make_assertion)
         assert await message.reply_venue(venue="test_venue")
@@ -1909,6 +1963,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_contact,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_contact,
@@ -1917,7 +1972,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_contact, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_contact, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_contact", make_assertion)
         assert await message.reply_contact(contact="test_contact")
@@ -1941,6 +1998,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_poll,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_poll,
@@ -1949,7 +2007,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_poll, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_poll, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_poll", make_assertion)
         assert await message.reply_poll(question="test_poll", options=["1", "2", "3"])
@@ -1972,6 +2032,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_dice,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_dice,
@@ -1980,7 +2041,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_dice, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_dice, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_dice", make_assertion)
         assert await message.reply_dice(disable_notification=True)
@@ -2007,6 +2070,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_chat_action,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             [],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_chat_action,
@@ -2014,7 +2078,9 @@ async def make_assertion(*_, **kwargs):
             "send_chat_action",
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_chat_action, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_chat_action, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_chat_action", make_assertion)
         assert await message.reply_chat_action(action=ChatAction.TYPING)
@@ -2038,6 +2104,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_game,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_game,
@@ -2046,7 +2113,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_game, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_game, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_game", make_assertion)
         assert await message.reply_game(game_short_name="test_game")
@@ -2078,6 +2147,7 @@ async def make_assertion(*_, **kwargs):
             Bot.send_invoice,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(
             message.reply_invoice,
@@ -2086,7 +2156,9 @@ async def make_assertion(*_, **kwargs):
             skip_params=["reply_to_message_id"],
             shortcut_kwargs=["business_connection_id"],
         )
-        assert await check_defaults_handling(message.reply_invoice, message.get_bot())
+        assert await check_defaults_handling(
+            message.reply_invoice, message.get_bot(), no_default_kwargs={"message_thread_id"}
+        )
 
         monkeypatch.setattr(message.get_bot(), "send_invoice", make_assertion)
         assert await message.reply_invoice(
@@ -2204,6 +2276,7 @@ async def make_assertion(*_, **kwargs):
             Bot.copy_message,
             ["chat_id", "reply_to_message_id", "business_connection_id"],
             ["quote", "do_quote", "reply_to_message_id"],
+            annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
         )
         assert await check_shortcut_call(message.copy, message.get_bot(), "copy_message")
         assert await check_defaults_handling(message.copy, message.get_bot())