Repository: aiogram/aiogram
Pull request #1030 (2022-10-18 06:55:46)

Add missing `Message` methods
# Add missing `Message` methods

Add following methods to `Message`:
```
Message.forward(...)
Message.edit_media(...)
Message.edit_live_location(...)
Message.stop_live_location(...)
Message.pin(...)
Message.unpin()
```
For each method add: 
- code
- test
- documentation

## Type of change
- New feature (non-breaking change that adds functionality)

# How has this been tested?
Add new tests in `tests/test_api/test_types/test_message.py`

## Test Configuration
Git hooks

# Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have added tests that prove my fix is effective or that my feature works as expected
- [x] New and existing unit tests pass locally with my changes
- [x] My changes generate no new warnings or errors
- [x] My changes are compatible with minimum requirements of the project
- [x] I have made corresponding changes to the documentation


github-actions[bot]: # :x: Changelog is required!

You need to add a brief description of the changes to the `CHANGES` directory.

For example, you can run `towncrier create <issue>.<type>` to create a file in the change directory and then write a description on that file.

Read more at [Towncrier docs](https://towncrier.readthedocs.io/en/latest/quickstart.html#creating-news-fragments)
codecov[bot]: # [Codecov](https://codecov.io/gh/aiogram/aiogram/pull/1030?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#1030](https://codecov.io/gh/aiogram/aiogram/pull/1030?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (a08b23f) into [dev-3.x](https://codecov.io/gh/aiogram/aiogram/commit/01028f10eb7a9b840a3532e31028dcb9f7ebd243?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (01028f1) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aiogram/aiogram/pull/1030/graphs/tree.svg?width=650&height=150&src=pr&token=cFqEr3p9MX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)](https://codecov.io/gh/aiogram/aiogram/pull/1030?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

```diff
@@            Coverage Diff            @@
##           dev-3.x     #1030   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          302       302           
  Lines         8140      8158   +18     
=========================================
+ Hits          8140      8158   +18     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `100.00% <100.00%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/aiogram/aiogram/pull/1030?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/types/message.py](https://codecov.io/gh/aiogram/aiogram/pull/1030/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS90eXBlcy9tZXNzYWdlLnB5) | `100.00% <100.00%> (ø)` | |

Danipulok: @JrooTJunior done

Patch:
diff --git a/CHANGES/1030.feature.rst b/CHANGES/1030.feature.rst
new file mode 100644
index 00000000..4007fc61
--- /dev/null
+++ b/CHANGES/1030.feature.rst
@@ -0,0 +1,8 @@
+Added following methods to ``Message`` class:
+
+- ``Message.forward(...)``
+- ``Message.edit_media(...)``
+- ``Message.edit_live_location(...)``
+- ``Message.stop_live_location(...)``
+- ``Message.pin(...)``
+- ``Message.unpin()``
diff --git a/aiogram/types/message.py b/aiogram/types/message.py
index 43ade2b4..b37af1d0 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -16,7 +16,11 @@ if TYPE_CHECKING:
         DeleteMessage,
         EditMessageCaption,
         EditMessageReplyMarkup,
+        EditMessageLiveLocation,
+        EditMessageMedia,
         EditMessageText,
+        ForwardMessage,
+        PinChatMessage,
         SendAnimation,
         SendAudio,
         SendContact,
@@ -34,6 +38,8 @@ if TYPE_CHECKING:
         SendVideo,
         SendVideoNote,
         SendVoice,
+        StopMessageLiveLocation,
+        UnpinChatMessage,
     )
     from .animation import Animation
     from .audio import Audio
@@ -45,6 +51,7 @@ if TYPE_CHECKING:
     from .game import Game
     from .inline_keyboard_markup import InlineKeyboardMarkup
     from .input_file import InputFile
+    from .input_media import InputMedia
     from .input_media_audio import InputMediaAudio
     from .input_media_document import InputMediaDocument
     from .input_media_photo import InputMediaPhoto
@@ -1822,6 +1829,36 @@ class Message(TelegramObject):
             reply_markup=reply_markup,
         )
 
+    def forward(
+        self,
+        chat_id: Union[int, str],
+        disable_notification: Optional[bool] = None,
+        protect_content: Optional[bool] = None,
+    ) -> ForwardMessage:
+        from ..methods import ForwardMessage
+
+        return ForwardMessage(
+            chat_id=chat_id,
+            from_chat_id=self.chat.id,
+            message_id=self.message_id,
+            disable_notification=disable_notification,
+            protect_content=protect_content,
+        )
+
+    def edit_media(
+        self,
+        media: InputMedia,
+        reply_markup: Optional[InlineKeyboardMarkup] = None,
+    ) -> EditMessageMedia:
+        from ..methods import EditMessageMedia
+
+        return EditMessageMedia(
+            media=media,
+            chat_id=self.chat.id,
+            message_id=self.message_id,
+            reply_markup=reply_markup,
+        )
+
     def edit_reply_markup(
         self,
         reply_markup: Optional[InlineKeyboardMarkup] = None,
@@ -1837,6 +1874,40 @@ class Message(TelegramObject):
     def delete_reply_markup(self) -> EditMessageReplyMarkup:
         return self.edit_reply_markup(reply_markup=None)
 
+    def edit_live_location(
+        self,
+        latitude: float,
+        longitude: float,
+        horizontal_accuracy: Optional[float] = None,
+        heading: Optional[int] = None,
+        proximity_alert_radius: Optional[int] = None,
+        reply_markup: Optional[InlineKeyboardMarkup] = None,
+    ) -> EditMessageLiveLocation:
+        from ..methods import EditMessageLiveLocation
+
+        return EditMessageLiveLocation(
+            latitude=latitude,
+            longitude=longitude,
+            chat_id=self.chat.id,
+            message_id=self.message_id,
+            horizontal_accuracy=horizontal_accuracy,
+            heading=heading,
+            proximity_alert_radius=proximity_alert_radius,
+            reply_markup=reply_markup,
+        )
+
+    def stop_live_location(
+        self,
+        reply_markup: Optional[InlineKeyboardMarkup] = None,
+    ) -> StopMessageLiveLocation:
+        from ..methods import StopMessageLiveLocation
+
+        return StopMessageLiveLocation(
+            chat_id=self.chat.id,
+            message_id=self.message_id,
+            reply_markup=reply_markup,
+        )
+
     def edit_caption(
         self,
         caption: str,
@@ -1860,6 +1931,26 @@ class Message(TelegramObject):
 
         return DeleteMessage(chat_id=self.chat.id, message_id=self.message_id)
 
+    def pin(
+        self,
+        disable_notification: Optional[bool] = None,
+    ) -> PinChatMessage:
+        from ..methods import PinChatMessage
+
+        return PinChatMessage(
+            chat_id=self.chat.id,
+            message_id=self.message_id,
+            disable_notification=disable_notification,
+        )
+
+    def unpin(self) -> UnpinChatMessage:
+        from ..methods import UnpinChatMessage
+
+        return UnpinChatMessage(
+            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.
diff --git a/docs/api/methods/edit_message_live_location.rst b/docs/api/methods/edit_message_live_location.rst
index 3d9ec289..65af8bf1 100644
--- a/docs/api/methods/edit_message_live_location.rst
+++ b/docs/api/methods/edit_message_live_location.rst
@@ -21,6 +21,14 @@ As bot method
     result: Union[Message, bool] = await bot.edit_message_live_location(...)
 
 
+As message method
+-----------------
+
+.. code-block::
+
+    result: Union[Message, bool] = await message.edit_live_location(...)
+
+
 Method as object
 ----------------
 
diff --git a/docs/api/methods/edit_message_media.rst b/docs/api/methods/edit_message_media.rst
index efd8a3cb..155a9eb6 100644
--- a/docs/api/methods/edit_message_media.rst
+++ b/docs/api/methods/edit_message_media.rst
@@ -21,6 +21,14 @@ As bot method
     result: Union[Message, bool] = await bot.edit_message_media(...)
 
 
+As message method
+-----------------
+
+.. code-block::
+
+    result: Union[Message, bool] = await message.edit_media(...)
+
+
 Method as object
 ----------------
 
diff --git a/docs/api/methods/forward_message.rst b/docs/api/methods/forward_message.rst
index 2b02dfdc..0242715c 100644
--- a/docs/api/methods/forward_message.rst
+++ b/docs/api/methods/forward_message.rst
@@ -21,6 +21,14 @@ As bot method
     result: Message = await bot.forward_message(...)
 
 
+As message method
+-------------
+
+.. code-block::
+
+    result: Message = await message.forward(...)
+
+
 Method as object
 ----------------
 
diff --git a/docs/api/methods/pin_chat_message.rst b/docs/api/methods/pin_chat_message.rst
index ec730278..d28eb9b4 100644
--- a/docs/api/methods/pin_chat_message.rst
+++ b/docs/api/methods/pin_chat_message.rst
@@ -21,6 +21,14 @@ As bot method
     result: bool = await bot.pin_chat_message(...)
 
 
+As message method
+-----------------
+
+.. code-block::
+
+    result: bool = await message.pin(...)
+
+
 Method as object
 ----------------
 
diff --git a/docs/api/methods/stop_message_live_location.rst b/docs/api/methods/stop_message_live_location.rst
index 1ef6440a..2cdc3fb4 100644
--- a/docs/api/methods/stop_message_live_location.rst
+++ b/docs/api/methods/stop_message_live_location.rst
@@ -21,6 +21,14 @@ As bot method
     result: Union[Message, bool] = await bot.stop_message_live_location(...)
 
 
+As message method
+-----------------
+
+.. code-block::
+
+    result: Union[Message, bool] = await message.stop_live_location(...)
+
+
 Method as object
 ----------------
 
diff --git a/docs/api/methods/unpin_chat_message.rst b/docs/api/methods/unpin_chat_message.rst
index 1531ec19..a4c5e7f1 100644
--- a/docs/api/methods/unpin_chat_message.rst
+++ b/docs/api/methods/unpin_chat_message.rst
@@ -21,6 +21,14 @@ As bot method
     result: bool = await bot.unpin_chat_message(...)
 
 
+As message method
+-----------------
+
+.. code-block::
+
+    result: bool = await message.unpin()
+
+
 Method as object
 ----------------
 


Test patch:
diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py
index cd078112..8eccb7d4 100644
--- a/tests/test_api/test_types/test_message.py
+++ b/tests/test_api/test_types/test_message.py
@@ -7,8 +7,12 @@ from aiogram.methods import (
     CopyMessage,
     DeleteMessage,
     EditMessageCaption,
+    EditMessageLiveLocation,
+    EditMessageMedia,
     EditMessageReplyMarkup,
     EditMessageText,
+    ForwardMessage,
+    PinChatMessage,
     SendAnimation,
     SendAudio,
     SendContact,
@@ -26,6 +30,8 @@ from aiogram.methods import (
     SendVideo,
     SendVideoNote,
     SendVoice,
+    StopMessageLiveLocation,
+    UnpinChatMessage,
     TelegramMethod,
 )
 from aiogram.types import (
@@ -39,6 +45,7 @@ from aiogram.types import (
     Game,
     InlineKeyboardButton,
     InlineKeyboardMarkup,
+    InputMediaPhoto,
     Invoice,
     Location,
     MessageAutoDeleteTimerChanged,
@@ -619,6 +626,23 @@ class TestMessage:
         assert isinstance(method, EditMessageText)
         assert method.chat_id == message.chat.id
 
+    def test_forward(self):
+        message = Message(
+            message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
+        )
+        method = message.forward(chat_id=69)
+        assert isinstance(method, ForwardMessage)
+        assert method.chat_id == 69
+        assert method.from_chat_id == message.chat.id
+
+    def test_edit_media(self):
+        message = Message(
+            message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
+        )
+        method = message.edit_media(media=InputMediaPhoto(media="photo.jpg"))
+        assert isinstance(method, EditMessageMedia)
+        assert method.chat_id == message.chat.id
+
     def test_edit_reply_markup(self):
         reply_markup = InlineKeyboardMarkup(
             inline_keyboard=[
@@ -677,6 +701,22 @@ class TestMessage:
         assert method.reply_markup is None
         assert method.chat_id == message.chat.id
 
+    def test_edit_live_location(self):
+        message = Message(
+            message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
+        )
+        method = message.edit_live_location(latitude=42, longitude=69)
+        assert isinstance(method, EditMessageLiveLocation)
+        assert method.chat_id == message.chat.id
+
+    def test_stop_live_location(self):
+        message = Message(
+            message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
+        )
+        method = message.stop_live_location()
+        assert isinstance(method, StopMessageLiveLocation)
+        assert method.chat_id == message.chat.id
+
     def test_edit_caption(self):
         message = Message(
             message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
@@ -694,6 +734,23 @@ class TestMessage:
         assert method.chat_id == message.chat.id
         assert method.message_id == message.message_id
 
+    def test_pin(self):
+        message = Message(
+            message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
+        )
+        method = message.pin()
+        assert isinstance(method, PinChatMessage)
+        assert method.chat_id == message.chat.id
+        assert method.message_id == message.message_id
+
+    def test_unpin(self):
+        message = Message(
+            message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
+        )
+        method = message.unpin()
+        assert isinstance(method, UnpinChatMessage)
+        assert method.chat_id == message.chat.id
+
     @pytest.mark.parametrize(
         "text,entities,mode,expected_value",
         [