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

Issue:
[FEATURE] Improve `Message.link` property to reflect threads
### What kind of feature are you missing? Where do you notice a shortcoming of PTB?

`Message.link` currently does not reflect if a message is a comment on another message in a supergroup/channel and also does not reflect if it's a message in a forum topic. By now we have a official documentation on how message links work: https://core.telegram.org/api/links#message-links and it would be nice to extend the support for this.

### Describe the solution you'd like

I'd like for `Message.link` to reflect the "threads" that the message was sent in. IISC that can achieved by

* include the `reply_to_message_id` as `thread` parameter in groups
* include the `reply_to_message_id` as `comment` parameter in channels
* for topics, I'm no entirely sure, but IISC the forum id and message_id should suffice, maybe the `forum_id` can also be used as `thread`

Admittedly, this needs some double checking on how things works out.

### Describe alternatives you've considered

We could also add a full-fledged `build_message_link` function to `helpers` (or sim naming) that accepts all parameters described in https://core.telegram.org/api/links#message-links and use that function in `Message.link`.

We could also provide a method `Message.build_link` that accepts boolean flags that indicate if the link should represent the message in the thread (if available) or in the whole chat etc, but I think that's over the top.

### Additional context

Originated from https://t.me/pythontelegrambotgroup/648879.

This has no priority to me. Just nice to have.


Discussion:

Resolving patch:
diff --git a/telegram/_message.py b/telegram/_message.py
--- a/telegram/_message.py
+++ b/telegram/_message.py
@@ -826,11 +826,20 @@ def id(self) -> int:  # pylint: disable=invalid-name
     def link(self) -> Optional[str]:
         """:obj:`str`: Convenience property. If the chat of the message is not
         a private chat or normal group, returns a t.me link of the message.
+
+            .. versionchanged:: NEXT.VERSION
+                For messages that are replies or part of a forum topic, the link now points
+                to the corresponding thread view.
         """
         if self.chat.type not in [Chat.PRIVATE, Chat.GROUP]:
             # the else block gets rid of leading -100 for supergroups:
             to_link = self.chat.username if self.chat.username else f"c/{str(self.chat.id)[4:]}"
-            return f"https://t.me/{to_link}/{self.message_id}"
+            baselink = f"https://t.me/{to_link}/{self.message_id}"
+
+            # adds the thread for topics and replies
+            if (self.is_topic_message and self.message_thread_id) or self.reply_to_message:
+                baselink = f"{baselink}?thread={self.message_thread_id}"
+            return baselink
         return None
 
     @classmethod


Test patch:
diff --git a/tests/test_message.py b/tests/test_message.py
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -791,6 +791,20 @@ def test_link_with_id(self, message, type_, id_):
         # The leading - for group ids/ -100 for supergroup ids isn't supposed to be in the link
         assert message.link == f"https://t.me/c/{3}/{message.message_id}"
 
+    def test_link_with_topics(self, message):
+        message.chat.username = None
+        message.chat.id = -1003
+        message.is_topic_message = True
+        message.message_thread_id = 123
+        assert message.link == f"https://t.me/c/3/{message.message_id}?thread=123"
+
+    def test_link_with_reply(self, message):
+        message.chat.username = None
+        message.chat.id = -1003
+        message.reply_to_message = Message(7, self.from_user, self.date, self.chat, text="Reply")
+        message.message_thread_id = 123
+        assert message.link == f"https://t.me/c/3/{message.message_id}?thread=123"
+
     @pytest.mark.parametrize(("id_", "username"), argvalues=[(None, "username"), (-3, None)])
     def test_link_private_chats(self, message, id_, username):
         message.chat.type = Chat.PRIVATE