Queer European MD passionate about IT
Selaa lähdekoodia

Compliance with bot API 7.1

- Added the class ReplyParameters and replaced parameters reply_to_message_id and allow_sending_without_reply in the methods copyMessage, sendMessage, sendPhoto, sendVideo, sendAnimation, sendAudio, sendDocument, sendSticker, sendVideoNote, sendVoice, sendLocation, sendVenue, sendContact, sendPoll, sendDice, sendInvoice, sendGame, and sendMediaGroup with the field reply_parameters of type ReplyParameters.
- Added the class LinkPreviewOptions and replaced the parameter disable_web_page_preview with link_preview_options in the methods sendMessage and editMessageText.
Davte 11 kuukautta sitten
vanhempi
commit
a343e095e8
3 muutettua tiedostoa jossa 415 lisäystä ja 120 poistoa
  1. 1 1
      davtelepot/__init__.py
  2. 336 70
      davtelepot/api.py
  3. 78 49
      davtelepot/bot.py

+ 1 - 1
davtelepot/__init__.py

@@ -11,7 +11,7 @@ __author__ = "Davide Testa"
 __email__ = "davide@davte.it"
 __credits__ = ["Marco Origlia", "Nick Lee @Nickoala"]
 __license__ = "GNU General Public License v3.0"
-__version__ = "2.9.12"
+__version__ = "2.10.1"
 __maintainer__ = "Davide Testa"
 __contact__ = "t.me/davte"
 

+ 336 - 70
davtelepot/api.py

@@ -7,6 +7,7 @@ A simple aiohttp asynchronous web client is used to make requests.
 # Standard library modules
 import asyncio
 import datetime
+import inspect
 import io
 import json
 import logging
@@ -349,6 +350,104 @@ class InlineQueryResultsButton(dict):
         return
 
 
+class DictToDump(dict):
+    def dumps(self):
+        parameters = {key: value for key, value in self.items() if value}
+        return json.dumps(parameters, separators=(',', ':'))
+
+
+class ReplyParameters(DictToDump):
+    def __init__(self, message_id: int,
+                 chat_id: Union[int, str] = None,
+                 allow_sending_without_reply: bool = None,
+                 quote: str = None,
+                 quote_parse_mode: str = None,
+                 quote_entities: list = None,
+                 quote_position: int = None):
+        super().__init__(self)
+        self['message_id'] = message_id
+        self['chat_id'] = chat_id
+        self['allow_sending_without_reply'] = allow_sending_without_reply
+        self['quote'] = quote
+        self['quote_parse_mode'] = quote_parse_mode
+        self['quote_entities'] = quote_entities
+        self['quote_position'] = quote_position
+
+
+class LinkPreviewOptions(DictToDump):
+    def __init__(self,
+                 is_disabled: bool = None,
+                 url: str = None,
+                 prefer_small_media: bool = None,
+                 prefer_large_media: bool = None,
+                 show_above_text: bool = None):
+        super().__init__(self)
+        self['is_disabled'] = is_disabled
+        self['url'] = url
+        self['prefer_small_media'] = prefer_small_media
+        self['prefer_large_media'] = prefer_large_media
+        self['show_above_text'] = show_above_text
+
+
+class ReactionType(DictToDump):
+    def __init__(self,
+                 type_: str,
+                 emoji: str = None,
+                 custom_emoji_id: str = None):
+        super().__init__(self)
+        if type_ not in ('emoji', 'custom_emoji'):
+            raise TypeError(
+            f"ReactionType must be `emoji` or `custom_emoji`.\n"
+            f"Unknown type {type_}"
+        )
+        self['type'] = type_
+        if emoji and custom_emoji_id:
+            raise TypeError(
+                "One and only one of the two fields `emoji` or `custom_emoji` "
+                "may be not None."
+            )
+        elif emoji:
+            self['emoji'] = emoji
+        elif custom_emoji_id:
+            self['custom_emoji_id'] = custom_emoji_id
+        else:
+            raise TypeError(
+                "At least one of the two fields `emoji` or `custom_emoji` "
+                "must be provided and not None."
+            )
+
+
+def handle_deprecated_disable_web_page_preview(parameters: dict,
+                                               kwargs: dict):
+    if 'disable_web_page_preview' in kwargs:
+        if parameters['link_preview_options'] is None:
+            parameters['link_preview_options'] = LinkPreviewOptions()
+        parameters['link_preview_options']['is_disabled'] = True
+        logging.error("DEPRECATION WARNING: `disable_web_page_preview` "
+                      f"parameter of function `{inspect.stack()[2][3]}` has been "
+                      "deprecated since Bot API 7.0. "
+                      "Use `link_preview_options` instead.")
+    return parameters
+
+
+def handle_deprecated_reply_parameters(parameters: dict,
+                                       kwargs: dict):
+    if 'reply_to_message_id' in kwargs and kwargs['reply_to_message_id']:
+        if parameters['reply_parameters'] is None:
+            parameters['reply_parameters'] = ReplyParameters(
+                message_id=kwargs['reply_to_message_id']
+            )
+        parameters['reply_parameters']['message_id'] = kwargs['reply_to_message_id']
+        if 'allow_sending_without_reply' in kwargs:
+            parameters['reply_parameters'][
+                'allow_sending_without_reply'
+            ] = kwargs['allow_sending_without_reply']
+        logging.error(f"DEPRECATION WARNING: `reply_to_message_id` and "
+                      f"`allow_sending_without_reply` parameters of function "
+                      f"`{inspect.stack()[2][3]}` have been deprecated since "
+                      f"Bot API 7.0. Use `reply_parameters` instead.")
+    return parameters
+
 
 # This class needs to mirror Telegram API, so camelCase method are needed
 # noinspection PyPep8Naming
@@ -491,6 +590,8 @@ class TelegramBot:
                 if (type(value) in (int, list,)
                         or (type(value) is dict and 'file' not in value)):
                     value = json.dumps(value, separators=(',', ':'))
+                elif isinstance(value, DictToDump):
+                    value = value.dumps()
                 data.add_field(key, value)
         return data
 
@@ -757,19 +858,27 @@ class TelegramBot:
                           message_thread_id: int = None,
                           parse_mode: str = None,
                           entities: List[dict] = None,
-                          disable_web_page_preview: bool = None,
+                          link_preview_options: LinkPreviewOptions = None,
                           disable_notification: bool = None,
                           protect_content: bool = None,
-                          reply_to_message_id: int = None,
-                          allow_sending_without_reply: bool = None,
-                          reply_markup=None):
+                          reply_parameters: ReplyParameters = None,
+                          reply_markup=None,
+                          **kwargs):
         """Send a text message. On success, return it.
 
         See https://core.telegram.org/bots/api#sendmessage for details.
         """
+        parameters = handle_deprecated_disable_web_page_preview(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
+        parameters = handle_deprecated_reply_parameters(
+            parameters=parameters,
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendMessage',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def forwardMessage(self, chat_id: Union[int, str],
@@ -794,17 +903,21 @@ class TelegramBot:
                         message_thread_id: int = None,
                         protect_content: bool = None,
                         disable_notification: bool = None,
-                        reply_to_message_id: int = None,
-                        allow_sending_without_reply: bool = None,
                         has_spoiler: bool = None,
-                        reply_markup=None):
+                        reply_parameters: ReplyParameters = None,
+                        reply_markup=None,
+                        **kwargs):
         """Send a photo from file_id, HTTP url or file.
 
         See https://core.telegram.org/bots/api#sendphoto for details.
         """
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendPhoto',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendAudio(self, chat_id: Union[int, str], audio,
@@ -816,10 +929,9 @@ class TelegramBot:
                         title: str = None,
                         thumbnail=None,
                         disable_notification: bool = None,
-                        reply_to_message_id: int = None,
-                        allow_sending_without_reply: bool = None,
                         message_thread_id: int = None,
                         protect_content: bool = None,
+                        reply_parameters: ReplyParameters = None,
                         reply_markup=None,
                         **kwargs):
         """Send an audio file from file_id, HTTP url or file.
@@ -831,9 +943,13 @@ class TelegramBot:
             logging.error("DEPRECATION WARNING: `thumb` parameter of function"
                           "`sendAudio` has been deprecated since Bot API 6.6. "
                           "Use `thumbnail` instead.")
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendAudio',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendDocument(self, chat_id: Union[int, str], document,
@@ -843,10 +959,9 @@ class TelegramBot:
                            caption_entities: List[dict] = None,
                            disable_content_type_detection: bool = None,
                            disable_notification: bool = None,
-                           reply_to_message_id: int = None,
-                           allow_sending_without_reply: bool = None,
                            message_thread_id: int = None,
                            protect_content: bool = None,
+                           reply_parameters: ReplyParameters = None,
                            reply_markup=None,
                            **kwargs):
         """Send a document from file_id, HTTP url or file.
@@ -858,9 +973,13 @@ class TelegramBot:
             logging.error("DEPRECATION WARNING: `thumb` parameter of function"
                           "`sendDocument` has been deprecated since Bot API 6.6. "
                           "Use `thumbnail` instead.")
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendDocument',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendVideo(self, chat_id: Union[int, str], video,
@@ -873,11 +992,10 @@ class TelegramBot:
                         caption_entities: List[dict] = None,
                         supports_streaming: bool = None,
                         disable_notification: bool = None,
-                        reply_to_message_id: int = None,
-                        allow_sending_without_reply: bool = None,
                         message_thread_id: int = None,
                         protect_content: bool = None,
                         has_spoiler: bool = None,
+                        reply_parameters: ReplyParameters = None,
                         reply_markup=None,
                         **kwargs):
         """Send a video from file_id, HTTP url or file.
@@ -889,9 +1007,13 @@ class TelegramBot:
             logging.error("DEPRECATION WARNING: `thumb` parameter of function"
                           "`sendVideo` has been deprecated since Bot API 6.6. "
                           "Use `thumbnail` instead.")
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendVideo',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendAnimation(self, chat_id: Union[int, str], animation,
@@ -903,11 +1025,10 @@ class TelegramBot:
                             parse_mode: str = None,
                             caption_entities: List[dict] = None,
                             disable_notification: bool = None,
-                            reply_to_message_id: int = None,
-                            allow_sending_without_reply: bool = None,
                             message_thread_id: int = None,
                             protect_content: bool = None,
                             has_spoiler: bool = None,
+                            reply_parameters: ReplyParameters = None,
                             reply_markup=None,
                             **kwargs):
         """Send animation files (GIF or H.264/MPEG-4 AVC video without sound).
@@ -919,9 +1040,13 @@ class TelegramBot:
             logging.error("DEPRECATION WARNING: `thumb` parameter of function"
                           "`sendAnimation` has been deprecated since Bot API 6.6. "
                           "Use `thumbnail` instead.")
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendAnimation',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendVoice(self, chat_id: Union[int, str], voice,
@@ -930,19 +1055,23 @@ class TelegramBot:
                         caption_entities: List[dict] = None,
                         duration: int = None,
                         disable_notification: bool = None,
-                        reply_to_message_id: int = None,
-                        allow_sending_without_reply: bool = None,
                         message_thread_id: int = None,
                         protect_content: bool = None,
-                        reply_markup=None):
+                        reply_parameters: ReplyParameters = None,
+                        reply_markup=None,
+                        **kwargs):
         """Send an audio file to be displayed as playable voice message.
 
         `voice` must be in an .ogg file encoded with OPUS.
         See https://core.telegram.org/bots/api#sendvoice for details.
         """
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendVoice',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendVideoNote(self, chat_id: Union[int, str], video_note,
@@ -950,10 +1079,9 @@ class TelegramBot:
                             length: int = None,
                             thumbnail=None,
                             disable_notification: bool = None,
-                            reply_to_message_id: int = None,
-                            allow_sending_without_reply: bool = None,
                             message_thread_id: int = None,
                             protect_content: bool = None,
+                            reply_parameters: ReplyParameters = None,
                             reply_markup=None,
                             **kwargs):
         """Send a rounded square mp4 video message of up to 1 minute long.
@@ -965,26 +1093,34 @@ class TelegramBot:
             logging.error("DEPRECATION WARNING: `thumb` parameter of function"
                           "`sendVideoNote` has been deprecated since Bot API 6.6. "
                           "Use `thumbnail` instead.")
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendVideoNote',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendMediaGroup(self, chat_id: Union[int, str], media: list,
                              disable_notification: bool = None,
-                             reply_to_message_id: int = None,
                              message_thread_id: int = None,
                              protect_content: bool = None,
-                             allow_sending_without_reply: bool = None):
+                             reply_parameters: ReplyParameters = None,
+                             **kwargs):
         """Send a group of photos or videos as an album.
 
         `media` must be a list of `InputMediaPhoto` and/or `InputMediaVideo`
             objects.
         See https://core.telegram.org/bots/api#sendmediagroup for details.
         """
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendMediaGroup',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendLocation(self, chat_id: Union[int, str],
@@ -994,11 +1130,11 @@ class TelegramBot:
                            heading: int = None,
                            proximity_alert_radius: int = None,
                            disable_notification: bool = None,
-                           reply_to_message_id: int = None,
-                           allow_sending_without_reply: bool = None,
                            message_thread_id: int = None,
                            protect_content: bool = None,
-                           reply_markup=None):
+                           reply_parameters: ReplyParameters = None,
+                           reply_markup=None,
+                           **kwargs):
         """Send a point on the map. May be kept updated for a `live_period`.
 
         See https://core.telegram.org/bots/api#sendlocation for details.
@@ -1011,9 +1147,13 @@ class TelegramBot:
             heading = max(1, min(heading, 360))
         if proximity_alert_radius:  # Distance 1-100000 m
             proximity_alert_radius = max(1, min(proximity_alert_radius, 100000))
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendLocation',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def editMessageLiveLocation(self, latitude: float, longitude: float,
@@ -1072,19 +1212,23 @@ class TelegramBot:
                         google_place_id: str = None,
                         google_place_type: str = None,
                         disable_notification: bool = None,
-                        reply_to_message_id: int = None,
-                        allow_sending_without_reply: bool = None,
                         message_thread_id: int = None,
                         protect_content: bool = None,
-                        reply_markup=None):
+                        reply_parameters: ReplyParameters = None,
+                        reply_markup=None,
+                        **kwargs):
         """Send information about a venue.
 
         Integrated with FourSquare.
         See https://core.telegram.org/bots/api#sendvenue for details.
         """
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendVenue',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendContact(self, chat_id: Union[int, str],
@@ -1093,18 +1237,22 @@ class TelegramBot:
                           last_name: str = None,
                           vcard: str = None,
                           disable_notification: bool = None,
-                          reply_to_message_id: int = None,
-                          allow_sending_without_reply: bool = None,
                           message_thread_id: int = None,
                           protect_content: bool = None,
-                          reply_markup=None):
+                          reply_parameters: ReplyParameters = None,
+                          reply_markup=None,
+                          **kwargs):
         """Send a phone contact.
 
         See https://core.telegram.org/bots/api#sendcontact for details.
         """
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendContact',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def sendPoll(self,
@@ -1122,11 +1270,11 @@ class TelegramBot:
                        close_date: Union[int, datetime.datetime] = None,
                        is_closed: bool = None,
                        disable_notification: bool = None,
-                       allow_sending_without_reply: bool = None,
-                       reply_to_message_id: int = None,
                        message_thread_id: int = None,
                        protect_content: bool = None,
-                       reply_markup=None):
+                       reply_parameters: ReplyParameters = None,
+                       reply_markup=None,
+                       **kwargs):
         """Send a native poll in a group, a supergroup or channel.
 
         See https://core.telegram.org/bots/api#sendpoll for details.
@@ -1150,6 +1298,10 @@ class TelegramBot:
         parameters = locals().copy()
         parameters['type'] = parameters['type_']
         del parameters['type_']
+        parameters = handle_deprecated_reply_parameters(
+            parameters=parameters,
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendPoll',
             parameters=parameters
@@ -1497,17 +1649,22 @@ class TelegramBot:
                               inline_message_id: str = None,
                               parse_mode: str = None,
                               entities: List[dict] = None,
-                              disable_web_page_preview: bool = None,
-                              reply_markup=None):
+                              link_preview_options: LinkPreviewOptions = None,
+                              reply_markup=None,
+                              **kwargs):
         """Edit text and game messages.
 
         On success, if edited message is sent by the bot, the edited Message
             is returned, otherwise True is returned.
         See https://core.telegram.org/bots/api#editmessagetext for details.
         """
+        parameters = handle_deprecated_disable_web_page_preview(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'editMessageText',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def editMessageCaption(self,
@@ -1604,15 +1761,28 @@ class TelegramBot:
             parameters=locals()
         )
 
+    async def deleteMessages(self, chat_id: Union[int, str],
+                             message_ids: List[int]):
+        """Delete multiple messages simultaneously.
+
+        If some of the specified messages can't be found, they are skipped.
+        Returns True on success.
+        See https://core.telegram.org/bots/api#deletemessages for details.
+        """
+        return await self.api_request(
+            'deleteMessages',
+            parameters=locals()
+        )
+
     async def sendSticker(self, chat_id: Union[int, str],
                           sticker: Union[str, dict, IO],
                           disable_notification: bool = None,
-                          reply_to_message_id: int = None,
-                          allow_sending_without_reply: bool = None,
                           message_thread_id: int = None,
                           protect_content: bool = None,
                           emoji: str = None,
-                          reply_markup=None):
+                          reply_parameters: ReplyParameters = None,
+                          reply_markup=None,
+                          **kwargs):
         """Send `.webp` stickers.
 
         `sticker` must be a file path, a URL, a file handle or a dict
@@ -1624,9 +1794,13 @@ class TelegramBot:
         if sticker is None:
             logging.error("Invalid sticker provided!")
             return
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         result = await self.api_request(
             'sendSticker',
-            parameters=locals()
+            parameters=parameters
         )
         if type(sticker) is dict:  # Close sticker file, if it was open
             sticker['file'].close()
@@ -1715,7 +1889,7 @@ class TelegramBot:
             raise TypeError(f"Unknown sticker type `{sticker_type}`.")
         result = await self.api_request(
             'createNewStickerSet',
-            parameters=locals(),
+            parameters=locals().copy(),
             exclude=['old_sticker_format']
         )
         return result
@@ -1749,7 +1923,7 @@ class TelegramBot:
             return
         result = await self.api_request(
             'addStickerToSet',
-            parameters=locals(),
+            parameters=locals().copy(),
             exclude=['old_sticker_format']
         )
         return result
@@ -1821,17 +1995,21 @@ class TelegramBot:
                           send_email_to_provider: bool = None,
                           is_flexible: bool = None,
                           disable_notification: bool = None,
-                          reply_to_message_id: int = None,
-                          allow_sending_without_reply: bool = None,
-                          reply_markup=None):
+                          reply_parameters: ReplyParameters = None,
+                          reply_markup=None,
+                          **kwargs):
         """Send an invoice.
 
         On success, the sent Message is returned.
         See https://core.telegram.org/bots/api#sendinvoice for details.
         """
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendInvoice',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def answerShippingQuery(self, shipping_query_id, ok,
@@ -1895,18 +2073,22 @@ class TelegramBot:
                        message_thread_id: int = None,
                        protect_content: bool = None,
                        disable_notification: bool = None,
-                       reply_to_message_id: int = None,
+                       reply_parameters: ReplyParameters = None,
                        reply_markup=None,
-                       allow_sending_without_reply: bool = None):
+                       **kwargs):
         """Send a game.
 
         On success, the sent Message is returned.
         See https://core.telegram.org/bots/api#sendgame for
             details.
         """
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendGame',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def setGameScore(self, user_id: int, score: int,
@@ -1954,11 +2136,11 @@ class TelegramBot:
                        chat_id: Union[int, str] = None,
                        emoji: str = None,
                        disable_notification: bool = None,
-                       reply_to_message_id: int = None,
-                       allow_sending_without_reply: bool = None,
                        message_thread_id: int = None,
                        protect_content: bool = None,
-                       reply_markup=None):
+                       reply_parameters: ReplyParameters = None,
+                       reply_markup=None,
+                       **kwargs):
         """Send a dice.
 
         Use this method to send a dice, which will have a random value from 1
@@ -1969,9 +2151,13 @@ class TelegramBot:
         See https://core.telegram.org/bots/api#senddice for
             details.
         """
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'sendDice',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def setChatAdministratorCustomTitle(self,
@@ -2100,9 +2286,9 @@ class TelegramBot:
                           parse_mode: str = None,
                           caption_entities: list = None,
                           disable_notification: bool = None,
-                          reply_to_message_id: int = None,
-                          allow_sending_without_reply: bool = None,
-                          reply_markup=None):
+                          reply_parameters: ReplyParameters = None,
+                          reply_markup=None,
+                          **kwargs):
         """Use this method to copy messages of any kind.
 
         The method is analogous to the method forwardMessages, but the copied
@@ -2110,9 +2296,13 @@ class TelegramBot:
         Returns the MessageId of the sent message on success.
         See https://core.telegram.org/bots/api#copymessage for details.
         """
+        parameters = handle_deprecated_reply_parameters(
+            parameters=locals().copy(),
+            kwargs=kwargs
+        )
         return await self.api_request(
             'copyMessage',
-            parameters=locals()
+            parameters=parameters
         )
 
     async def unpinAllChatMessages(self, chat_id: Union[int, str]):
@@ -2700,3 +2890,79 @@ class TelegramBot:
             'unpinAllGeneralForumTopicMessages',
             parameters=locals()
         )
+
+    async def getUserChatBoosts(self, chat_id: Union[int, str], user_id: int):
+        """Get the list of boosts added to a chat by a user.
+
+        Requires administrator rights in the chat.
+        Returns a UserChatBoosts object.
+        See https://core.telegram.org/bots/api#getuserchatboosts for details.
+        """
+        return await self.api_request(
+            'getUserChatBoosts',
+            parameters=locals()
+        )
+
+    async def forwardMessages(self, chat_id: Union[int, str],
+                              from_chat_id: Union[int, str],
+                              message_ids: List[int],
+                              message_thread_id: int = None,
+                              disable_notification: bool = None,
+                              protect_content: bool = None):
+        """Forward multiple messages of any kind.
+
+        If some of the specified messages can't be found or forwarded, they are
+            skipped.
+        Service messages and messages with protected content can't be
+            forwarded.
+        Album grouping is kept for forwarded messages.
+        On success, an array of MessageId of the sent messages is returned.
+        See https://core.telegram.org/bots/api#forwardmessages for details.
+        """
+        return await self.api_request(
+            'forwardMessages',
+            parameters=locals()
+        )
+
+    async def copyMessages(self, chat_id: Union[int, str],
+                           from_chat_id: Union[int, str],
+                           message_ids: List[int],
+                           message_thread_id: int = None,
+                           disable_notification: bool = None,
+                           protect_content: bool = None,
+                           remove_caption: bool = None):
+        """Copy messages of any kind.
+
+        If some of the specified messages can't be found or copied, they are
+            skipped.
+        Service messages, giveaway messages, giveaway winners messages, and
+            invoice messages can't be copied.
+        A quiz poll can be copied only if the value of the field
+            correct_option_id is known to the bot.
+        The method is analogous to the method forwardMessages, but the copied
+            messages don't have a link to the original message.
+        Album grouping is kept for copied messages.
+        On success, an array of MessageId of the sent messages is returned.
+        See https://core.telegram.org/bots/api#copymessages for details.
+        """
+        return await self.api_request(
+            'copyMessages',
+            parameters=locals()
+        )
+
+    async def setMessageReaction(self, chat_id: Union[int, str],
+                                 message_id: int,
+                                 reaction: List[ReactionType] = None,
+                                 is_big: bool = None):
+        """Change the chosen reactions on a message.
+
+        Service messages can't be reacted to.
+        Automatically forwarded messages from a channel to its discussion group
+            have the same available reactions as messages in the channel.
+        Returns True on success.
+        See https://core.telegram.org/bots/api#setmessagereaction for details.
+        """
+        return await self.api_request(
+            'setMessageReaction',
+            parameters=locals()
+        )

+ 78 - 49
davtelepot/bot.py

@@ -2,34 +2,6 @@
 
 camelCase methods mirror API directly, while snake_case ones act as middleware
     someway.
-
-Usage
-    ```
-    import sys
-
-    from davtelepot.bot import Bot
-
-    from data.passwords import my_token, my_other_token
-
-    long_polling_bot = Bot(token=my_token, database_url='my_db')
-    webhook_bot = Bot(token=my_other_token, hostname='example.com',
-                      certificate='path/to/certificate.pem',
-                      database_url='my_other_db')
-
-    @long_polling_bot.command('/foo')
-    async def foo_command(bot, update, user_record, language):
-        return "Bar!"
-
-    @webhook_bot.command('/bar')
-    async def bar_command(bot, update, user_record, language):
-        return "Foo!"
-
-    exit_state = Bot.run(
-        local_host='127.0.0.5',
-        port=8552
-    )
-    sys.exit(exit_state)
-    ```
 """
 
 # Standard library modules
@@ -49,7 +21,9 @@ from typing import Callable, List, Union, Dict
 import aiohttp.web
 
 # Project modules
-from davtelepot.api import TelegramBot, TelegramError
+from davtelepot.api import (
+    LinkPreviewOptions, ReplyParameters, TelegramBot, TelegramError
+)
 from davtelepot.database import ObjectWithDatabase
 from davtelepot.languages import MultiLanguageObject
 from davtelepot.messages import davtelepot_messages
@@ -1319,19 +1293,21 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
 
     async def send_message(self, chat_id: Union[int, str] = None,
                            text: str = None,
+                           message_thread_id: int = None,
                            entities: List[dict] = None,
                            parse_mode: str = 'HTML',
-                           message_thread_id: int = None,
+                           link_preview_options: LinkPreviewOptions = None,
+                           disable_notification: bool = None,
                            protect_content: bool = None,
                            disable_web_page_preview: bool = None,
-                           disable_notification: bool = None,
                            reply_to_message_id: int = None,
                            allow_sending_without_reply: bool = None,
-                           reply_markup=None,
                            update: dict = None,
                            reply_to_update: bool = False,
                            send_default_keyboard: bool = True,
-                           user_record: OrderedDict = None):
+                           user_record: OrderedDict = None,
+                           reply_parameters: ReplyParameters = None,
+                           reply_markup=None):
         """Send text via message(s).
 
         This method wraps lower-level `TelegramBot.sendMessage` method.
@@ -1352,6 +1328,10 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
             user_record = self.db['users'].find_one(telegram_id=chat_id)
         if reply_to_update and 'message_id' in update:
             reply_to_message_id = update['message_id']
+        if disable_web_page_preview:
+            if link_preview_options is None:
+                link_preview_options = LinkPreviewOptions()
+            link_preview_options['is_disabled'] = True
         if (
             send_default_keyboard
             and reply_markup is None
@@ -1395,19 +1375,27 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
             limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 100,
             parse_mode=parse_mode
         )
+        if reply_to_message_id:
+            if reply_parameters is None:
+                reply_parameters = ReplyParameters(message_id=reply_to_message_id)
+            reply_parameters['message_id'] = reply_to_message_id
+            if allow_sending_without_reply:
+                reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
+            if reply_to_update and 'chat' in update and 'id' in update['chat']:
+                if update['chat']['id'] != chat_id:
+                    reply_parameters['chat_id'] = update['chat']['id']
         for text_chunk, is_last in text_chunks:
             _reply_markup = (reply_markup if is_last else None)
             sent_message_update = await self.sendMessage(
                 chat_id=chat_id,
                 text=text_chunk,
+                message_thread_id=message_thread_id,
                 parse_mode=parse_mode,
                 entities=entities,
-                message_thread_id=message_thread_id,
-                protect_content=protect_content,
-                disable_web_page_preview=disable_web_page_preview,
+                link_preview_options=link_preview_options,
                 disable_notification=disable_notification,
-                reply_to_message_id=reply_to_message_id,
-                allow_sending_without_reply=allow_sending_without_reply,
+                protect_content=protect_content,
+                reply_parameters=reply_parameters,
                 reply_markup=_reply_markup
             )
         return sent_message_update
@@ -1431,6 +1419,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                                 parse_mode: str = 'HTML',
                                 entities: List[dict] = None,
                                 disable_web_page_preview: bool = None,
+                                link_preview_options: LinkPreviewOptions = None,
                                 allow_sending_without_reply: bool = None,
                                 reply_markup=None,
                                 update: dict = None):
@@ -1463,6 +1452,10 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
             )
         ):
             if i == 0:
+                if disable_web_page_preview:
+                    if link_preview_options is None:
+                        link_preview_options = LinkPreviewOptions()
+                    link_preview_options['is_disabled'] = True
                 edited_message = await self.editMessageText(
                     text=text_chunk,
                     chat_id=chat_id,
@@ -1470,7 +1463,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                     inline_message_id=inline_message_id,
                     parse_mode=parse_mode,
                     entities=entities,
-                    disable_web_page_preview=disable_web_page_preview,
+                    link_preview_options=link_preview_options,
                     reply_markup=(reply_markup if is_last else None)
                 )
                 if chat_id is None:
@@ -1576,6 +1569,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                          reply_markup=None,
                          update: dict = None,
                          reply_to_update: bool = False,
+                         reply_parameters: ReplyParameters = None,
                          send_default_keyboard: bool = True,
                          use_stored_file_id: bool = True):
         """Send photos.
@@ -1599,6 +1593,15 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
             chat_id = self.get_chat_id(update)
         if reply_to_update and 'message_id' in update:
             reply_to_message_id = update['message_id']
+        if reply_to_message_id:
+            if reply_parameters is None:
+                reply_parameters = ReplyParameters(message_id=reply_to_message_id)
+            reply_parameters['message_id'] = reply_to_message_id
+            if allow_sending_without_reply:
+                reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
+            if reply_to_update and 'chat' in update and 'id' in update['chat']:
+                if update['chat']['id'] != chat_id:
+                    reply_parameters['chat_id'] = update['chat']['id']
         if (
             send_default_keyboard
             and reply_markup is None
@@ -1652,8 +1655,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                 parse_mode=parse_mode,
                 caption_entities=caption_entities,
                 disable_notification=disable_notification,
-                reply_to_message_id=reply_to_message_id,
-                allow_sending_without_reply=allow_sending_without_reply,
+                reply_parameters=reply_parameters,
                 reply_markup=reply_markup
             )
             if isinstance(sent_update, Exception):
@@ -1701,6 +1703,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                          reply_markup=None,
                          update: dict = None,
                          reply_to_update: bool = False,
+                         reply_parameters: ReplyParameters = None,
                          send_default_keyboard: bool = True,
                          use_stored_file_id: bool = True):
         """Send audio files.
@@ -1724,6 +1727,15 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
             chat_id = self.get_chat_id(update)
         if reply_to_update and 'message_id' in update:
             reply_to_message_id = update['message_id']
+        if reply_to_message_id:
+            if reply_parameters is None:
+                reply_parameters = ReplyParameters(message_id=reply_to_message_id)
+            reply_parameters['message_id'] = reply_to_message_id
+            if allow_sending_without_reply:
+                reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
+            if reply_to_update and 'chat' in update and 'id' in update['chat']:
+                if update['chat']['id'] != chat_id:
+                    reply_parameters['chat_id'] = update['chat']['id']
         if (
             send_default_keyboard
             and reply_markup is None
@@ -1781,8 +1793,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                 title=title,
                 thumbnail=thumbnail,
                 disable_notification=disable_notification,
-                reply_to_message_id=reply_to_message_id,
-                allow_sending_without_reply=allow_sending_without_reply,
+                reply_parameters=reply_parameters,
                 reply_markup=reply_markup
             )
             if isinstance(sent_update, Exception):
@@ -1826,6 +1837,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                          reply_markup=None,
                          update: dict = None,
                          reply_to_update: bool = False,
+                         reply_parameters: ReplyParameters = None,
                          send_default_keyboard: bool = True,
                          use_stored_file_id: bool = True):
         """Send voice messages.
@@ -1849,6 +1861,15 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
             chat_id = self.get_chat_id(update)
         if reply_to_update and 'message_id' in update:
             reply_to_message_id = update['message_id']
+        if reply_to_message_id:
+            if reply_parameters is None:
+                reply_parameters = ReplyParameters(message_id=reply_to_message_id)
+            reply_parameters['message_id'] = reply_to_message_id
+            if allow_sending_without_reply:
+                reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
+            if reply_to_update and 'chat' in update and 'id' in update['chat']:
+                if update['chat']['id'] != chat_id:
+                    reply_parameters['chat_id'] = update['chat']['id']
         if (
             send_default_keyboard
             and reply_markup is None
@@ -1903,8 +1924,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                 caption_entities=caption_entities,
                 duration=duration,
                 disable_notification=disable_notification,
-                reply_to_message_id=reply_to_message_id,
-                allow_sending_without_reply=allow_sending_without_reply,
+                reply_parameters=reply_parameters,
                 reply_markup=reply_markup
             )
             if isinstance(sent_update, Exception):
@@ -1951,6 +1971,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                             document_name: str = None,
                             update: dict = None,
                             reply_to_update: bool = False,
+                            reply_parameters: ReplyParameters = None,
                             send_default_keyboard: bool = True,
                             use_stored_file_id: bool = False):
         """Send a document.
@@ -1983,6 +2004,15 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
             return
         if reply_to_update and 'message_id' in update:
             reply_to_message_id = update['message_id']
+        if reply_to_message_id:
+            if reply_parameters is None:
+                reply_parameters = ReplyParameters(message_id=reply_to_message_id)
+            reply_parameters['message_id'] = reply_to_message_id
+            if allow_sending_without_reply:
+                reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
+            if reply_to_update and 'chat' in update and 'id' in update['chat']:
+                if update['chat']['id'] != chat_id:
+                    reply_parameters['chat_id'] = update['chat']['id']
         if chat_id > 0:
             user_record = self.db['users'].find_one(telegram_id=chat_id)
             language = self.get_language(update=update, user_record=user_record)
@@ -2061,7 +2091,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                                     caption=caption,
                                     parse_mode=parse_mode,
                                     disable_notification=disable_notification,
-                                    reply_to_message_id=reply_to_message_id,
+                                    reply_parameters=reply_parameters,
                                     reply_markup=reply_markup,
                                     update=update,
                                     reply_to_update=reply_to_update,
@@ -2092,8 +2122,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
                 caption_entities=caption_entities,
                 disable_content_type_detection=disable_content_type_detection,
                 disable_notification=disable_notification,
-                reply_to_message_id=reply_to_message_id,
-                allow_sending_without_reply=allow_sending_without_reply,
+                reply_parameters=reply_parameters,
                 reply_markup=reply_markup
             )
             if isinstance(sent_update, Exception):
@@ -3505,7 +3534,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
         Each bot will receive updates via long polling or webhook according to
             its initialization parameters.
         A single aiohttp.web.Application instance will be run (cls.app) on
-            local_host:port and it may serve custom-defined routes as well.
+            local_host:port, and it may serve custom-defined routes as well.
         """
         if local_host is not None:
             cls.local_host = local_host