diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 7d735ac3..4c0db8c9 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -54,3 +54,19 @@ class Chat(TelegramObject): """*Optional*. Unique identifier for the linked chat, i.e. the discussion group identifier for a channel and vice versa; for supergroups and channel chats. This identifier may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" location: Optional[ChatLocation] = None """*Optional*. For supergroups, the location to which the supergroup is connected. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" + + @property + def shifted_id(self) -> int: + """ + Returns shifted chat ID (positive and without "-100" prefix). + Mostly used for private links like t.me/c/chat_id/message_id + + Currently supergroup/channel IDs have 10-digit ID after "-100" prefix removed. + However, these IDs might become 11-digit in future. So, first we remove "-100" + prefix and count remaining number length. Then we multiple + -1 * 10 ^ (number_length + 2) + Finally, self.id is substracted from that number + """ + short_id = str(self.id).replace("-100", "") + shift = int(-1 * pow(10, len(short_id) + 2)) + return shift - self.id diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 67b3a594..8d72d0e5 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1760,6 +1760,25 @@ class Message(TelegramObject): return DeleteMessage(chat_id=self.chat.id, message_id=self.message_id) + def get_url(self, force_private: bool = False) -> Optional[str]: + """ + Returns message URL. Cannot be used in private (one-to-one) chats. + If chat has a username, returns URL like https://t.me/username/message_id + Otherwise (or if {force_private} flag is set), returns https://t.me/c/shifted_chat_id/message_id + + :param force_private: if set, a private URL is returned even for a public chat + :return: string with full message URL + """ + if self.chat.type in ("private", "group"): + return None + + if not self.chat.username or force_private: + chat_value = f"c/{self.chat.shifted_id}" + else: + chat_value = self.chat.username + + return f"https://t.me/{chat_value}/{self.message_id}" + class ContentType(helper.Helper): mode = helper.HelperMode.snake_case diff --git a/tests/test_api/test_methods/test_get_url.py b/tests/test_api/test_methods/test_get_url.py new file mode 100644 index 00000000..3c769ca2 --- /dev/null +++ b/tests/test_api/test_methods/test_get_url.py @@ -0,0 +1,51 @@ +import datetime +from typing import Optional + +import pytest +from aiogram.types import Chat, Message + +from tests.mocked_bot import MockedBot + + +class TestGetMessageUrl: + @pytest.mark.parametrize( + "chat_type,chat_id,chat_username,force_private,expected_result", + [ + ["private", 123456, "username", False, None], + ["group", -123456, "username", False, None], + ["supergroup", -1001234567890, None, False, "https://t.me/c/1234567890/10"], + ["supergroup", -1001234567890, None, True, "https://t.me/c/1234567890/10"], + ["supergroup", -1001234567890, "username", False, "https://t.me/username/10"], + ["supergroup", -1001234567890, "username", True, "https://t.me/c/1234567890/10"], + ["channel", -1001234567890, None, False, "https://t.me/c/1234567890/10"], + ["channel", -1001234567890, None, True, "https://t.me/c/1234567890/10"], + ["channel", -1001234567890, "username", False, "https://t.me/username/10"], + ["channel", -1001234567890, "username", True, "https://t.me/c/1234567890/10"], + # 2 extra cases: 9-digit ID and 11-digit ID (without "-100") + ["supergroup", -100123456789, None, True, "https://t.me/c/123456789/10"], + ["supergroup", -10012345678901, None, True, "https://t.me/c/12345678901/10"], + ], + ) + def test_method( + self, + bot: MockedBot, + chat_type: str, + chat_id: int, + chat_username: Optional[str], + force_private: bool, + expected_result: Optional[str], + ): + + fake_chat = Chat(id=chat_id, username=chat_username, type=chat_type) + fake_message_id = 10 + fake_message = Message( + message_id=fake_message_id, + date=datetime.datetime.now(), + text="test", + chat=fake_chat, + ) + + if expected_result is None: + assert fake_message.get_url(force_private=force_private) is None + else: + assert fake_message.get_url(force_private=force_private) == expected_result