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

Issue:
[FEATURE] Shortcuts for Message
#### Is your feature request related to a problem? Please describe.
My most wanted shortcut is `Message.reply_chat_action`, but going through the list, think we can also add `Message.reply_{game, invoice}`. 

#### Describe the solution you'd like
Update `Message` with the new shortcuts.

#### Describe alternatives you've considered
N/A

#### Additional context
N/A

Happy to make a PR if you think this is suitable.



Discussion:
> Happy to make a PR if you think this is suitable.

we absolutely do, feel free to give it a shot

Resolving patch:
diff --git a/telegram/chat.py b/telegram/chat.py
--- a/telegram/chat.py
+++ b/telegram/chat.py
@@ -651,7 +651,7 @@ def send_chat_action(
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`.
 
         Returns:
-            :obj:`True`: On success.
+            :obj:`bool`: On success, :obj:`True` is returned.
 
         """
         return self.bot.send_chat_action(
diff --git a/telegram/message.py b/telegram/message.py
--- a/telegram/message.py
+++ b/telegram/message.py
@@ -69,6 +69,7 @@
         InputMediaDocument,
         InputMediaPhoto,
         InputMediaVideo,
+        LabeledPrice,
     )
 
 _UNDEFINED = object()
@@ -617,7 +618,7 @@ def reply_text(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_message(update.message.chat_id, *args, **kwargs)
+            bot.send_message(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
 
@@ -661,8 +662,12 @@ def reply_markdown(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_message(update.message.chat_id, parse_mode=ParseMode.MARKDOWN, *args,
-            **kwargs)
+            bot.send_message(
+                update.effective_message.chat_id,
+                parse_mode=ParseMode.MARKDOWN,
+                *args,
+                **kwargs,
+            )
 
         Sends a message with Markdown version 1 formatting.
 
@@ -711,8 +716,12 @@ def reply_markdown_v2(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_message(update.message.chat_id, parse_mode=ParseMode.MARKDOWN_V2, *args,
-            **kwargs)
+            bot.send_message(
+                update.effective_message.chat_id,
+                parse_mode=ParseMode.MARKDOWN_V2,
+                *args,
+                **kwargs,
+            )
 
         Sends a message with markdown version 2 formatting.
 
@@ -757,7 +766,12 @@ def reply_html(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_message(update.message.chat_id, parse_mode=ParseMode.HTML, *args, **kwargs)
+            bot.send_message(
+                update.effective_message.chat_id,
+                parse_mode=ParseMode.HTML,
+                *args,
+                **kwargs,
+            )
 
         Sends a message with HTML formatting.
 
@@ -801,7 +815,7 @@ def reply_media_group(
     ) -> List['Message']:
         """Shortcut for::
 
-            bot.send_media_group(update.message.chat_id, *args, **kwargs)
+            bot.send_media_group(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`.
 
@@ -845,7 +859,7 @@ def reply_photo(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_photo(update.message.chat_id, *args, **kwargs)
+            bot.send_photo(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`.
 
@@ -896,7 +910,7 @@ def reply_audio(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_audio(update.message.chat_id, *args, **kwargs)
+            bot.send_audio(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`.
 
@@ -949,7 +963,7 @@ def reply_document(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_document(update.message.chat_id, *args, **kwargs)
+            bot.send_document(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`.
 
@@ -1002,7 +1016,7 @@ def reply_animation(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_animation(update.message.chat_id, *args, **kwargs)
+            bot.send_animation(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`.
 
@@ -1049,7 +1063,7 @@ def reply_sticker(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_sticker(update.message.chat_id, *args, **kwargs)
+            bot.send_sticker(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`.
 
@@ -1097,7 +1111,7 @@ def reply_video(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_video(update.message.chat_id, *args, **kwargs)
+            bot.send_video(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`.
 
@@ -1149,7 +1163,7 @@ def reply_video_note(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_video_note(update.message.chat_id, *args, **kwargs)
+            bot.send_video_note(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`.
 
@@ -1197,7 +1211,7 @@ def reply_voice(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_voice(update.message.chat_id, *args, **kwargs)
+            bot.send_voice(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`.
 
@@ -1247,7 +1261,7 @@ def reply_location(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_location(update.message.chat_id, *args, **kwargs)
+            bot.send_location(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`.
 
@@ -1300,7 +1314,7 @@ def reply_venue(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_venue(update.message.chat_id, *args, **kwargs)
+            bot.send_venue(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`.
 
@@ -1351,7 +1365,7 @@ def reply_contact(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_contact(update.message.chat_id, *args, **kwargs)
+            bot.send_contact(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`.
 
@@ -1405,7 +1419,7 @@ def reply_poll(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_poll(update.message.chat_id, *args, **kwargs)
+            bot.send_poll(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`.
 
@@ -1455,7 +1469,7 @@ def reply_dice(
     ) -> 'Message':
         """Shortcut for::
 
-            bot.send_dice(update.message.chat_id, *args, **kwargs)
+            bot.send_dice(update.effective_message.chat_id, *args, **kwargs)
 
         For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`.
 
@@ -1481,6 +1495,149 @@ def reply_dice(
             allow_sending_without_reply=allow_sending_without_reply,
         )
 
+    def reply_chat_action(
+        self,
+        action: str,
+        timeout: float = None,
+        api_kwargs: JSONDict = None,
+    ) -> bool:
+        """Shortcut for::
+
+            bot.send_chat_action(update.effective_message.chat_id, *args, **kwargs)
+
+        For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`.
+
+        .. versionadded:: 13.2
+
+        Returns:
+            :obj:`bool`: On success, :obj:`True` is returned.
+
+        """
+        return self.bot.send_chat_action(
+            chat_id=self.chat_id,
+            action=action,
+            timeout=timeout,
+            api_kwargs=api_kwargs,
+        )
+
+    def reply_game(
+        self,
+        game_short_name: str,
+        disable_notification: bool = False,
+        reply_to_message_id: Union[int, str] = None,
+        reply_markup: 'InlineKeyboardMarkup' = None,
+        timeout: float = None,
+        api_kwargs: JSONDict = None,
+        allow_sending_without_reply: bool = None,
+        quote: bool = None,
+    ) -> 'Message':
+        """Shortcut for::
+
+            bot.send_game(update.effective_message.chat_id, *args, **kwargs)
+
+        For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`.
+
+        Args:
+            quote (:obj:`bool`, optional): If set to :obj:`True`, the game is sent as an actual
+                reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+                parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False`
+                in private chats.
+
+        .. versionadded:: 13.2
+
+        Returns:
+            :class:`telegram.Message`: On success, instance representing the message posted.
+
+        """
+        reply_to_message_id = self._quote(quote, reply_to_message_id)
+        return self.bot.send_game(
+            chat_id=self.chat_id,
+            game_short_name=game_short_name,
+            disable_notification=disable_notification,
+            reply_to_message_id=reply_to_message_id,
+            reply_markup=reply_markup,
+            timeout=timeout,
+            api_kwargs=api_kwargs,
+            allow_sending_without_reply=allow_sending_without_reply,
+        )
+
+    def reply_invoice(
+        self,
+        title: str,
+        description: str,
+        payload: str,
+        provider_token: str,
+        start_parameter: str,
+        currency: str,
+        prices: List['LabeledPrice'],
+        photo_url: str = None,
+        photo_size: int = None,
+        photo_width: int = None,
+        photo_height: int = None,
+        need_name: bool = None,
+        need_phone_number: bool = None,
+        need_email: bool = None,
+        need_shipping_address: bool = None,
+        is_flexible: bool = None,
+        disable_notification: bool = False,
+        reply_to_message_id: Union[int, str] = None,
+        reply_markup: 'InlineKeyboardMarkup' = None,
+        provider_data: Union[str, object] = None,
+        send_phone_number_to_provider: bool = None,
+        send_email_to_provider: bool = None,
+        timeout: float = None,
+        api_kwargs: JSONDict = None,
+        allow_sending_without_reply: bool = None,
+        quote: bool = None,
+    ) -> 'Message':
+        """Shortcut for::
+
+            bot.send_invoice(update.effective_message.chat_id, *args, **kwargs)
+
+        For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`.
+
+        Args:
+            quote (:obj:`bool`, optional): If set to :obj:`True`, the invoice is sent as an actual
+                reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+                parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False`
+                in private chats.
+
+        .. versionadded:: 13.2
+
+        Returns:
+            :class:`telegram.Message`: On success, instance representing the message posted.
+
+        """
+        reply_to_message_id = self._quote(quote, reply_to_message_id)
+        return self.bot.send_invoice(
+            chat_id=self.chat_id,
+            title=title,
+            description=description,
+            payload=payload,
+            provider_token=provider_token,
+            start_parameter=start_parameter,
+            currency=currency,
+            prices=prices,
+            photo_url=photo_url,
+            photo_size=photo_size,
+            photo_width=photo_width,
+            photo_height=photo_height,
+            need_name=need_name,
+            need_phone_number=need_phone_number,
+            need_email=need_email,
+            need_shipping_address=need_shipping_address,
+            is_flexible=is_flexible,
+            disable_notification=disable_notification,
+            reply_to_message_id=reply_to_message_id,
+            reply_markup=reply_markup,
+            provider_data=provider_data,
+            send_phone_number_to_provider=send_phone_number_to_provider,
+            send_email_to_provider=send_email_to_provider,
+            timeout=timeout,
+            api_kwargs=api_kwargs,
+            allow_sending_without_reply=allow_sending_without_reply,
+        )
+
     def forward(
         self,
         chat_id: Union[int, str],
@@ -1491,8 +1648,8 @@ def forward(
         """Shortcut for::
 
             bot.forward_message(chat_id=chat_id,
-                                from_chat_id=update.message.chat_id,
-                                message_id=update.message.message_id,
+                                from_chat_id=update.effective_message.chat_id,
+                                message_id=update.effective_message.message_id,
                                 *args,
                                 **kwargs)
 
@@ -1527,8 +1684,8 @@ def copy(
         """Shortcut for::
 
             bot.copy_message(chat_id=chat_id,
-                             from_chat_id=update.message.chat_id,
-                             message_id=update.message.message_id,
+                             from_chat_id=update.effective_message.chat_id,
+                             message_id=update.effective_message.message_id,
                              *args,
                              **kwargs)
 
diff --git a/telegram/utils/request.py b/telegram/utils/request.py
--- a/telegram/utils/request.py
+++ b/telegram/utils/request.py
@@ -130,14 +130,14 @@ def __init__(
         # TODO: Support other platforms like mac and windows.
         if 'linux' in sys.platform:
             sockopts.append(
-                (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 120)
-            )  # pylint: disable=no-member
+                (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 120)  # pylint: disable=no-member
+            )
             sockopts.append(
-                (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30)
-            )  # pylint: disable=no-member
+                (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30)  # pylint: disable=no-member
+            )
             sockopts.append(
-                (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 8)
-            )  # pylint: disable=no-member
+                (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 8)  # pylint: disable=no-member
+            )
 
         self._con_pool_size = con_pool_size
 


Test patch:
diff --git a/tests/test_message.py b/tests/test_message.py
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -47,6 +47,7 @@
     ProximityAlertTriggered,
     Dice,
     Bot,
+    ChatAction,
 )
 from telegram.ext import Defaults
 from tests.conftest import check_shortcut_signature, check_shortcut_call
@@ -1047,6 +1048,88 @@ def make_assertion(*_, **kwargs):
         assert message.reply_dice(disable_notification=True)
         assert message.reply_dice(disable_notification=True, quote=True)
 
+    def test_reply_action(self, monkeypatch, message: Message):
+        send_chat_action = message.bot.send_chat_action
+
+        def make_assertion(*_, **kwargs):
+            id_ = kwargs['chat_id'] == message.chat_id
+            action = kwargs['action'] == ChatAction.TYPING
+            return id_ and action and check_shortcut_call(kwargs, send_chat_action)
+
+        assert check_shortcut_signature(
+            Message.reply_chat_action, Bot.send_chat_action, ['chat_id'], []
+        )
+
+        monkeypatch.setattr(message.bot, 'send_chat_action', make_assertion)
+        assert message.reply_chat_action(action=ChatAction.TYPING)
+
+    def test_reply_game(self, monkeypatch, message: Message):
+        send_game = message.bot.send_game
+
+        def make_assertion(*_, **kwargs):
+            return (
+                kwargs['chat_id'] == message.chat_id
+                and kwargs['game_short_name'] == 'test_game'
+                and check_shortcut_call(kwargs, send_game)
+            )
+
+        assert check_shortcut_signature(Message.reply_game, Bot.send_game, ['chat_id'], ['quote'])
+
+        monkeypatch.setattr(message.bot, 'send_game', make_assertion)
+        assert message.reply_game(game_short_name='test_game')
+        assert message.reply_game(game_short_name='test_game', quote=True)
+
+    def test_reply_invoice(self, monkeypatch, message: Message):
+        send_invoice = message.bot.send_invoice
+
+        def make_assertion(*_, **kwargs):
+            title = kwargs['title'] == 'title'
+            description = kwargs['description'] == 'description'
+            payload = kwargs['payload'] == 'payload'
+            provider_token = kwargs['provider_token'] == 'provider_token'
+            start_parameter = kwargs['start_parameter'] == 'start_parameter'
+            currency = kwargs['currency'] == 'currency'
+            prices = kwargs['prices'] == 'prices'
+            args = (
+                title
+                and description
+                and payload
+                and provider_token
+                and start_parameter
+                and currency
+                and prices
+            )
+            return (
+                kwargs['chat_id'] == message.chat_id
+                and args
+                and check_shortcut_call(kwargs, send_invoice)
+            )
+
+        assert check_shortcut_signature(
+            Message.reply_invoice, Bot.send_invoice, ['chat_id'], ['quote']
+        )
+
+        monkeypatch.setattr(message.bot, 'send_invoice', make_assertion)
+        assert message.reply_invoice(
+            'title',
+            'description',
+            'payload',
+            'provider_token',
+            'start_parameter',
+            'currency',
+            'prices',
+        )
+        assert message.reply_invoice(
+            'title',
+            'description',
+            'payload',
+            'provider_token',
+            'start_parameter',
+            'currency',
+            'prices',
+            quote=True,
+        )
+
     @pytest.mark.parametrize('disable_notification', [False, True])
     def test_forward(self, monkeypatch, message, disable_notification):
         forward_message = message.bot.forward_message