From bfad2fa1f3b3c12dd18781ceb3cc45c026d9d2c1 Mon Sep 17 00:00:00 2001 From: Eldinnie Date: Sat, 14 Oct 2017 20:03:02 +0200 Subject: [PATCH] support 3.4 API (#865) --- README.rst | 2 +- telegram/bot.py | 178 +++++++++++++++++- telegram/chat.py | 11 ++ telegram/inline/inlinequeryresultlocation.py | 7 + .../inline/inputlocationmessagecontent.py | 5 +- telegram/message.py | 72 ++++++- tests/test_bot.py | 17 +- tests/test_chat.py | 17 +- tests/test_inlinequeryresultlocation.py | 5 + tests/test_inputlocationmessagecontent.py | 7 +- tests/test_location.py | 64 +++++++ tests/test_message.py | 37 +++- tests/test_official.py | 16 +- 13 files changed, 403 insertions(+), 35 deletions(-) diff --git a/README.rst b/README.rst index e5991314a..8c0e0d760 100644 --- a/README.rst +++ b/README.rst @@ -96,7 +96,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -As of **23. July 2017**, all types and methods of the Telegram Bot API 3.2 are supported. +All types and methods of the Telegram Bot API 3.4 are supported. ========== Installing diff --git a/telegram/bot.py b/telegram/bot.py index 8af33b8d3..9cc15b4c0 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -768,6 +768,7 @@ class Bot(TelegramObject): reply_markup=None, timeout=None, location=None, + live_period=None, **kwargs): """Use this method to send point on the map. @@ -780,6 +781,8 @@ class Bot(TelegramObject): latitude (:obj:`float`, optional): Latitude of location. longitude (:obj:`float`, optional): Longitude of location. location (:class:`telegram.Location`, optional): The location to send. + live_period (:obj:`int`, optional): Period in seconds for which the location will be + updated, should be between 60 and 86400. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the @@ -803,7 +806,11 @@ class Bot(TelegramObject): if not (all([latitude, longitude]) or location): raise ValueError("Either location or latitude and longitude must be passed as" - "argument") + "argument.") + + if not ((latitude is not None or longitude is not None) ^ bool(location)): + raise ValueError("Either location or latitude and longitude must be passed as" + "argument. Not both.") if isinstance(location, Location): latitude = location.latitude @@ -811,6 +818,114 @@ class Bot(TelegramObject): data = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude} + if live_period: + data['live_period'] = live_period + + return url, data + + @log + @message + def edit_message_live_location(self, + chat_id=None, + message_id=None, + inline_message_id=None, + latitude=None, + longitude=None, + location=None, + reply_markup=None, + **kwargs): + """Use this method to edit live location messages sent by the bot or via the bot + (for inline bots). A location can be edited until its :attr:`live_period` expires or + editing is explicitly disabled by a call to :attr:`stop_message_live_location`. + + Note: + You can either supply a :obj:`latitude` and :obj:`longitude` or a :obj:`location`. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format @channelusername). + message_id (:obj:`int`, optional): Required if inline_message_id is not specified. + Identifier of the sent message. + inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not + specified. Identifier of the inline message. + latitude (:obj:`float`, optional): Latitude of location. + longitude (:obj:`float`, optional): Longitude of location. + location (:class:`telegram.Location`, optional): The location to send. + reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A + JSON-serialized object for an inline keyboard, custom reply keyboard, instructions + to remove reply keyboard or to force a reply from the user. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + + Returns: + :class:`telegram.Message`: On success the edited message. + """ + + url = '{0}/editMessageLiveLocation'.format(self.base_url) + + if not (all([latitude, longitude]) or location): + raise ValueError("Either location or latitude and longitude must be passed as" + "argument.") + if not ((latitude is not None or longitude is not None) ^ bool(location)): + raise ValueError("Either location or latitude and longitude must be passed as" + "argument. Not both.") + + if isinstance(location, Location): + latitude = location.latitude + longitude = location.longitude + + data = {'latitude': latitude, 'longitude': longitude} + + if chat_id: + data['chat_id'] = chat_id + if message_id: + data['message_id'] = message_id + if inline_message_id: + data['inline_message_id'] = inline_message_id + + return url, data + + @log + @message + def stop_message_live_location(self, + chat_id=None, + message_id=None, + inline_message_id=None, + reply_markup=None, + **kwargs): + """Use this method to stop updating a live location message sent by the bot or via the bot + (for inline bots) before live_period expires. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format @channelusername). + message_id (:obj:`int`, optional): Required if inline_message_id is not specified. + Identifier of the sent message. + inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not + specified. Identifier of the inline message. + reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A + JSON-serialized object for an inline keyboard, custom reply keyboard, instructions + to remove reply keyboard or to force a reply from the user. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + + Returns: + :class:`telegram.Message`: On success the edited message. + """ + + url = '{0}/stopMessageLiveLocation'.format(self.base_url) + + data = {} + + if chat_id: + data['chat_id'] = chat_id + if message_id: + data['message_id'] = message_id + if inline_message_id: + data['inline_message_id'] = inline_message_id + return url, data @log @@ -1825,6 +1940,63 @@ class Bot(TelegramObject): return ChatMember.de_json(result, self) + @log + def set_chat_sticker_set(self, chat_id, sticker_set_name, timeout=None, **kwargs): + """Use this method to set a new group sticker set for a supergroup. + The bot must be an administrator in the chat for this to work and must have the appropriate + admin rights. Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned + in :attr:`get_chat` requests to check if the bot can use this method. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target supergroup (in the format @supergroupusername). + sticker_set_name (:obj:`str`): Name of the sticker set to be set as the group + sticker set. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + + Returns: + :obj:`bool`: True on success. + """ + + url = '{0}/setChatStickerSet'.format(self.base_url) + + data = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name} + + result = self._request.post(url, data, timeout=timeout) + + return result + + @log + def delete_chat_sticker_set(self, chat_id, timeout=None, **kwargs): + """Use this method to delete a group sticker set from a supergroup. The bot must be an + administrator in the chat for this to work and must have the appropriate admin rights. + Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned in + :attr:`get_chat` requests to check if the bot can use this method. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target supergroup (in the format @supergroupusername). + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Returns: + :obj:`bool`: True on success. + """ + + url = '{0}/deleteChatStickerSet'.format(self.base_url) + + data = {'chat_id': chat_id} + + result = self._request.post(url, data, timeout=timeout) + + return result + def get_webhook_info(self, timeout=None, **kwargs): """Use this method to get current webhook status. Requires no parameters. @@ -2794,6 +2966,8 @@ class Bot(TelegramObject): sendVoice = send_voice sendVideoNote = send_video_note sendLocation = send_location + editMessageLiveLocation = edit_message_live_location + stopMessageLiveLocation = stop_message_live_location sendVenue = send_venue sendContact = send_contact sendGame = send_game @@ -2814,6 +2988,8 @@ class Bot(TelegramObject): getChat = get_chat getChatAdministrators = get_chat_administrators getChatMember = get_chat_member + setChatStickerSet = set_chat_sticker_set + deleteChatStickerSet = delete_chat_sticker_set getChatMembersCount = get_chat_members_count getWebhookInfo = get_webhook_info setGameScore = set_game_score diff --git a/telegram/chat.py b/telegram/chat.py index d0a2e5850..b1db4dca1 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -38,6 +38,9 @@ class Chat(TelegramObject): invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats. pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups. Returned only in get_chat. + sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. + can_set_sticker_set (:obj:`bool`): Optional. ``True``, if the bot can change group the + sticker set. Args: id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits @@ -61,6 +64,10 @@ class Chat(TelegramObject): pinned_message (:class:`telegram.Message`, optional): Pinned message, for supergroups. Returned only in get_chat. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + sticker_set_name (:obj:`str`, optional): For supergroups, name of Group sticker set. + Returned only in get_chat. + can_set_sticker_set (:obj:`bool`, optional): ``True``, if the bot can change group the + sticker set. Returned only in get_chat. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ @@ -87,6 +94,8 @@ class Chat(TelegramObject): description=None, invite_link=None, pinned_message=None, + sticker_set_name=None, + can_set_sticker_set=None, **kwargs): # Required self.id = int(id) @@ -101,6 +110,8 @@ class Chat(TelegramObject): self.description = description self.invite_link = invite_link self.pinned_message = pinned_message + self.sticker_set_name = sticker_set_name + self.can_set_sticker_set = can_set_sticker_set self.bot = bot self._id_attrs = (self.id,) diff --git a/telegram/inline/inlinequeryresultlocation.py b/telegram/inline/inlinequeryresultlocation.py index 44b5d45e6..4616ac2be 100644 --- a/telegram/inline/inlinequeryresultlocation.py +++ b/telegram/inline/inlinequeryresultlocation.py @@ -33,6 +33,8 @@ class InlineQueryResultLocation(InlineQueryResult): latitude (:obj:`float`): Location latitude in degrees. longitude (:obj:`float`): Location longitude in degrees. title (:obj:`str`): Location title. + live_period (:obj:`int`): Optional. Period in seconds for which the location can be + updated, should be between 60 and 86400. reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -46,6 +48,8 @@ class InlineQueryResultLocation(InlineQueryResult): latitude (:obj:`float`): Location latitude in degrees. longitude (:obj:`float`): Location longitude in degrees. title (:obj:`str`): Location title. + live_period (:obj:`int`, optional): Period in seconds for which the location can be + updated, should be between 60 and 86400. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -62,6 +66,7 @@ class InlineQueryResultLocation(InlineQueryResult): latitude, longitude, title, + live_period=None, reply_markup=None, input_message_content=None, thumb_url=None, @@ -75,6 +80,8 @@ class InlineQueryResultLocation(InlineQueryResult): self.title = title # Optionals + if live_period: + self.live_period = live_period if reply_markup: self.reply_markup = reply_markup if input_message_content: diff --git a/telegram/inline/inputlocationmessagecontent.py b/telegram/inline/inputlocationmessagecontent.py index 1155f4ad9..437c53bbd 100644 --- a/telegram/inline/inputlocationmessagecontent.py +++ b/telegram/inline/inputlocationmessagecontent.py @@ -32,11 +32,14 @@ class InputLocationMessageContent(InputMessageContent): Args: latitude (:obj:`float`): Latitude of the location in degrees. longitude (:obj:`float`): Longitude of the location in degrees. + live_period (:obj:`int`, optional): Period in seconds for which the location can be + updated, should be between 60 and 86400. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, latitude, longitude, **kwargs): + def __init__(self, latitude, longitude, live_period=None, **kwargs): # Required self.latitude = latitude self.longitude = longitude + self.live_period = live_period diff --git a/telegram/message.py b/telegram/message.py index 580988138..d912a8812 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -26,7 +26,6 @@ from telegram import (Audio, Contact, Document, Chat, Location, PhotoSize, Stick from telegram.utils.deprecate import warn_deprecate_obj from telegram.utils.helpers import escape_html, escape_markdown, to_timestamp, from_timestamp - _UNDEFINED = object() @@ -54,6 +53,10 @@ class Message(TelegramObject): usernames, URLs, bot commands, etc. that appear in the text. See :attr:`Message.parse_entity` and :attr:`parse_entities` methods for how to use properly. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like + usernames, URLs, bot commands, etc. that appear in the caption. See + :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` methods for how + to use properly. audio (:class:`telegram.Audio`): Optional. Information about the file. document (:class:`telegram.Document`): Optional. Information about the file. game (:class:`telegram.Game`): Optional. Information about the game. @@ -119,6 +122,10 @@ class Message(TelegramObject): entities (List[:class:`telegram.MessageEntity`], optional): For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text. See attr:`parse_entity` and attr:`parse_entities` methods for how to use properly. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. For Messages with a + Caption. Special entities like usernames, URLs, bot commands, etc. that appear in the + caption. See :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` + methods for how to use properly. audio (:class:`telegram.Audio`, optional): Message is an audio file, information about the file. document (:class:`telegram.Document`, optional): Message is a general file, information @@ -196,6 +203,7 @@ class Message(TelegramObject): edit_date=None, text=None, entities=None, + caption_entities=None, audio=None, document=None, game=None, @@ -239,6 +247,7 @@ class Message(TelegramObject): self.edit_date = edit_date self.text = text self.entities = entities or list() + self.caption_entities = caption_entities or list() self.audio = audio self.game = game self.document = document @@ -289,6 +298,7 @@ class Message(TelegramObject): data['date'] = from_timestamp(data['date']) data['chat'] = Chat.de_json(data.get('chat'), bot) data['entities'] = MessageEntity.de_list(data.get('entities'), bot) + data['caption_entities'] = MessageEntity.de_list(data.get('caption_entities'), bot) data['forward_from'] = User.de_json(data.get('forward_from'), bot) data['forward_from_chat'] = Chat.de_json(data.get('forward_from_chat'), bot) data['forward_date'] = from_timestamp(data.get('forward_date')) @@ -369,6 +379,8 @@ class Message(TelegramObject): data['photo'] = [p.to_dict() for p in self.photo] if self.entities: data['entities'] = [e.to_dict() for e in self.entities] + if self.caption_entities: + data['caption_entities'] = [e.to_dict() for e in self.caption_entities] if self.new_chat_photo: data['new_chat_photo'] = [p.to_dict() for p in self.new_chat_photo] data['new_chat_member'] = data.pop('_new_chat_member', None) @@ -683,7 +695,7 @@ class Message(TelegramObject): be an entity that belongs to this message. Returns: - str: The text of the given entity + :obj:`str`: The text of the given entity """ # Is it a narrow build, if so we don't need to convert @@ -695,6 +707,31 @@ class Message(TelegramObject): return entity_text.decode('utf-16-le') + def parse_caption_entity(self, entity): + """Returns the text from a given :class:`telegram.MessageEntity`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``Message.caption`` with the offset and length.) + + Args: + entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must + be an entity that belongs to this message. + + Returns: + :obj:`str`: The text of the given entity + + """ + # Is it a narrow build, if so we don't need to convert + if sys.maxunicode == 0xffff: + return self.caption[entity.offset:entity.offset + entity.length] + else: + entity_text = self.caption.encode('utf-16-le') + entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2] + + return entity_text.decode('utf-16-le') + def parse_entities(self, types=None): """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. @@ -726,6 +763,37 @@ class Message(TelegramObject): for entity in self.entities if entity.type in types } + def parse_caption_entities(self, types=None): + """ + Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. + It contains entities from this message's caption filtered by their + :attr:`telegram.MessageEntity.type` attribute as the key, and the text that each entity + belongs to as the value of the :obj:`dict`. + + Note: + This method should always be used instead of the :attr:`caption_entities` attribute, + since it calculates the correct substring from the message text based on UTF-16 + codepoints. See :attr:`parse_entity` for more info. + + Args: + types (List[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as + strings. If the ``type`` attribute of an entity is contained in this list, it will + be returned. Defaults to a list of all types. All types can be found as constants + in :class:`telegram.MessageEntity`. + + Returns: + Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to + the text that belongs to them, calculated based on UTF-16 codepoints. + + """ + if types is None: + types = MessageEntity.ALL_TYPES + + return { + entity: self.parse_caption_entity(entity) + for entity in self.caption_entities if entity.type in types + } + def _text_html(self, urled=False): entities = self.parse_entities() message_text = self.text diff --git a/tests/test_bot.py b/tests/test_bot.py index 2067cbe15..6d547ba16 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -107,15 +107,6 @@ class TestBot(object): # send_photo, send_audio, send_document, send_sticker, send_video, send_voice # and send_video_note are tested in their respective test modules. No need to duplicate here. - @flaky(3, 1) - @pytest.mark.timeout(10) - def test_send_location(self, bot, chat_id): - message = bot.send_location(chat_id=chat_id, latitude=-23.691288, longitude=-46.788279) - - assert message.location - assert message.location.longitude == -46.788279 - assert message.location.latitude == -23.691288 - @flaky(3, 1) @pytest.mark.timeout(10) def test_send_venue(self, bot, chat_id): @@ -368,6 +359,14 @@ class TestBot(object): assert chat_member.status == 'administrator' assert chat_member.user.username == 'EchteEldin' + @pytest.mark.skip(reason="Not implemented yet.") + def test_set_chat_sticker_set(self): + pass + + @pytest.mark.skip(reason="Not implemented yet.") + def test_delete_chat_sticker_set(self): + pass + @pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (yet)') @flaky(3, 1) @pytest.mark.timeout(10) diff --git a/tests/test_chat.py b/tests/test_chat.py index ee9b29ccd..dd4e3ee22 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -27,7 +27,8 @@ from telegram import User def chat(bot): return Chat(TestChat.id, TestChat.title, TestChat.type, all_members_are_administrators=TestChat.all_members_are_administrators, - bot=bot) + bot=bot, sticker_set_name=TestChat.sticker_set_name, + can_set_sticker_set=TestChat.can_set_sticker_set) class TestChat(object): @@ -35,13 +36,17 @@ class TestChat(object): title = 'ToledosPalaceBot - Group' type = 'group' all_members_are_administrators = False + sticker_set_name = 'stickers' + can_set_sticker_set = False def test_de_json(self, bot): json_dict = { - 'id': TestChat.id, - 'title': TestChat.title, - 'type': TestChat.type, - 'all_members_are_administrators': TestChat.all_members_are_administrators + 'id': self.id, + 'title': self.title, + 'type': self.type, + 'all_members_are_administrators': self.all_members_are_administrators, + 'sticker_set_name': self.sticker_set_name, + 'can_set_sticker_set': self.can_set_sticker_set } chat = Chat.de_json(json_dict, bot) @@ -49,6 +54,8 @@ class TestChat(object): assert chat.title == self.title assert chat.type == self.type assert chat.all_members_are_administrators == self.all_members_are_administrators + assert chat.sticker_set_name == self.sticker_set_name + assert chat.can_set_sticker_set == self.can_set_sticker_set def test_to_dict(self, chat): chat_dict = chat.to_dict() diff --git a/tests/test_inlinequeryresultlocation.py b/tests/test_inlinequeryresultlocation.py index f3ededca7..f06e9fe8f 100644 --- a/tests/test_inlinequeryresultlocation.py +++ b/tests/test_inlinequeryresultlocation.py @@ -29,6 +29,7 @@ def inline_query_result_location(): TestInlineQueryResultLocation.latitude, TestInlineQueryResultLocation.longitude, TestInlineQueryResultLocation.title, + live_period=TestInlineQueryResultLocation.live_period, thumb_url=TestInlineQueryResultLocation.thumb_url, thumb_width=TestInlineQueryResultLocation.thumb_width, thumb_height=TestInlineQueryResultLocation.thumb_height, @@ -42,6 +43,7 @@ class TestInlineQueryResultLocation(object): latitude = 0.0 longitude = 1.0 title = 'title' + live_period = 70 thumb_url = 'thumb url' thumb_width = 10 thumb_height = 15 @@ -54,6 +56,7 @@ class TestInlineQueryResultLocation(object): assert inline_query_result_location.latitude == self.latitude assert inline_query_result_location.longitude == self.longitude assert inline_query_result_location.title == self.title + assert inline_query_result_location.live_period == self.live_period assert inline_query_result_location.thumb_url == self.thumb_url assert inline_query_result_location.thumb_width == self.thumb_width assert inline_query_result_location.thumb_height == self.thumb_height @@ -72,6 +75,8 @@ class TestInlineQueryResultLocation(object): assert inline_query_result_location_dict['longitude'] == \ inline_query_result_location.longitude assert inline_query_result_location_dict['title'] == inline_query_result_location.title + assert inline_query_result_location_dict[ + 'live_period'] == inline_query_result_location.live_period assert inline_query_result_location_dict['thumb_url'] == \ inline_query_result_location.thumb_url assert inline_query_result_location_dict['thumb_width'] == \ diff --git a/tests/test_inputlocationmessagecontent.py b/tests/test_inputlocationmessagecontent.py index 0af425b78..78e73e95e 100644 --- a/tests/test_inputlocationmessagecontent.py +++ b/tests/test_inputlocationmessagecontent.py @@ -25,16 +25,19 @@ from telegram import InputLocationMessageContent @pytest.fixture(scope='class') def input_location_message_content(): return InputLocationMessageContent(TestInputLocationMessageContent.latitude, - TestInputLocationMessageContent.longitude) + TestInputLocationMessageContent.longitude, + live_period=TestInputLocationMessageContent.live_period) class TestInputLocationMessageContent(object): latitude = -23.691288 longitude = -46.788279 + live_period = 80 def test_expected_values(self, input_location_message_content): assert input_location_message_content.longitude == self.longitude assert input_location_message_content.latitude == self.latitude + assert input_location_message_content.live_period == self.live_period def test_to_dict(self, input_location_message_content): input_location_message_content_dict = input_location_message_content.to_dict() @@ -44,3 +47,5 @@ class TestInputLocationMessageContent(object): input_location_message_content.latitude assert input_location_message_content_dict['longitude'] == \ input_location_message_content.longitude + assert input_location_message_content_dict[ + 'live_period'] == input_location_message_content.live_period diff --git a/tests/test_location.py b/tests/test_location.py index 901c6002e..a0e5a42d4 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -18,8 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. import pytest +from flaky import flaky from telegram import Location +from telegram.error import BadRequest @pytest.fixture(scope='class') @@ -39,6 +41,46 @@ class TestLocation(object): assert location.latitude == self.latitude assert location.longitude == self.longitude + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_send_live_location(self, bot, chat_id): + message = bot.send_location(chat_id=chat_id, latitude=52.223880, longitude=5.166146, + live_period=80) + assert message.location + assert message.location.latitude == 52.223880 + assert message.location.longitude == 5.166146 + + message2 = bot.edit_message_live_location(message.chat_id, message.message_id, + latitude=52.223098, longitude=5.164306) + + assert message2.location.latitude == 52.223098 + assert message2.location.longitude == 5.164306 + + bot.stop_message_live_location(message.chat_id, message.message_id) + with pytest.raises(BadRequest, match="Message can't be edited"): + bot.edit_message_live_location(message.chat_id, message.message_id, latitude=52.223880, + longitude=5.164306) + + # TODO: Needs improvement with in inline sent live location. + def test_edit_live_inline_message(self, monkeypatch, bot, location): + def test(_, url, data, **kwargs): + lat = data['latitude'] == location.latitude + lon = data['longitude'] == location.longitude + id = data['inline_message_id'] == 1234 + return lat and lon and id + + monkeypatch.setattr('telegram.utils.request.Request.post', test) + assert bot.edit_message_live_location(inline_message_id=1234, location=location) + + # TODO: Needs improvement with in inline sent live location. + def test_stop_live_inline_message(self, monkeypatch, bot): + def test(_, url, data, **kwargs): + id = data['inline_message_id'] == 1234 + return id + + monkeypatch.setattr('telegram.utils.request.Request.post', test) + assert bot.stop_message_live_location(inline_message_id=1234) + def test_send_with_location(self, monkeypatch, bot, chat_id, location): def test(_, url, data, **kwargs): lat = data['latitude'] == location.latitude @@ -48,10 +90,32 @@ class TestLocation(object): monkeypatch.setattr('telegram.utils.request.Request.post', test) assert bot.send_location(location=location, chat_id=chat_id) + def test_edit_live_location_with_location(self, monkeypatch, bot, location): + def test(_, url, data, **kwargs): + lat = data['latitude'] == location.latitude + lon = data['longitude'] == location.longitude + return lat and lon + + monkeypatch.setattr('telegram.utils.request.Request.post', test) + assert bot.edit_message_live_location(None, None, location=location) + def test_send_location_without_required(self, bot, chat_id): with pytest.raises(ValueError, match='Either location or latitude and longitude'): bot.send_location(chat_id=chat_id) + def test_edit_location_without_required(self, bot): + with pytest.raises(ValueError, match='Either location or latitude and longitude'): + bot.edit_message_live_location(chat_id=2, message_id=3) + + def test_send_location_with_all_args(self, bot, location): + with pytest.raises(ValueError, match='Not both'): + bot.send_location(chat_id=1, latitude=2.5, longitude=4.6, location=location) + + def test_edit_location_with_all_args(self, bot, location): + with pytest.raises(ValueError, match='Not both'): + bot.edit_message_live_location(chat_id=1, message_id=7, latitude=2.5, longitude=4.6, + location=location) + def test_to_dict(self, location): location_dict = location.to_dict() diff --git a/tests/test_message.py b/tests/test_message.py index 1dc2a8e49..fb9c1c74c 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -40,9 +40,12 @@ def message(bot): 'forward_date': datetime.now()}, {'reply_to_message': Message(50, None, None, None)}, {'edit_date': datetime.now()}, - {'test': 'a text message', + {'text': 'a text message', 'enitites': [MessageEntity('bold', 10, 4), MessageEntity('italic', 16, 7)]}, + {'caption': 'A message caption', + 'caption_entities': [MessageEntity('bold', 1, 1), + MessageEntity('text_link', 4, 3)]}, {'audio': Audio('audio_id', 12), 'caption': 'audio_file'}, {'document': Document('document_id'), @@ -78,12 +81,13 @@ def message(bot): {'forward_signature': 'some_forward_sign'}, {'author_signature': 'some_author_sign'} ], - ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text', 'audio', - 'document', 'game', 'photo', 'sticker', 'video', 'voice', 'video_note', - 'new_members', 'contact', 'location', 'venue', 'left_member', 'new_title', - 'new_photo', 'delete_photo', 'group_created', 'supergroup_created', - 'channel_created', 'migrated_to', 'migrated_from', 'pinned', 'invoice', - 'successful_payment', 'forward_signature', 'author_signature']) + ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text', + 'caption_entities', 'audio', 'document', 'game', 'photo', 'sticker', 'video', + 'voice', 'video_note', 'new_members', 'contact', 'location', 'venue', + 'left_member', 'new_title', 'new_photo', 'delete_photo', 'group_created', + 'supergroup_created', 'channel_created', 'migrated_to', 'migrated_from', + 'pinned', 'invoice', 'successful_payment', 'forward_signature', + 'author_signature']) def message_params(bot, request): return Message(message_id=TestMessage.id, from_user=TestMessage.from_user, @@ -127,6 +131,14 @@ class TestMessage(object): message = Message(1, self.from_user, self.date, self.chat, text=text, entities=[entity]) assert message.parse_entity(entity) == 'http://google.com' + def test_parse_caption_entity(self): + caption = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467' + b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape') + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + message = Message(1, self.from_user, self.date, self.chat, caption=caption, + caption_entities=[entity]) + assert message.parse_caption_entity(entity) == 'http://google.com' + def test_parse_entities(self): text = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467' b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape') @@ -137,6 +149,16 @@ class TestMessage(object): assert message.parse_entities(MessageEntity.URL) == {entity: 'http://google.com'} assert message.parse_entities() == {entity: 'http://google.com', entity_2: 'h'} + def test_parse_caption_entities(self): + text = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467' + b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape') + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1) + message = Message(1, self.from_user, self.date, self.chat, + caption=text, caption_entities=[entity_2, entity]) + assert message.parse_caption_entities(MessageEntity.URL) == {entity: 'http://google.com'} + assert message.parse_caption_entities() == {entity: 'http://google.com', entity_2: 'h'} + def test_text_html_simple(self): test_html_string = ('Test for <bold, ita_lic, code, ' 'links and
pre
. ' @@ -201,7 +223,6 @@ class TestMessage(object): item = None assert message_params.effective_attachment == item - def test_reply_text(self, monkeypatch, message): def test(*args, **kwargs): id = args[1] == message.chat_id diff --git a/tests/test_official.py b/tests/test_official.py index 0931c45bd..8a5f7c1f5 100644 --- a/tests/test_official.py +++ b/tests/test_official.py @@ -33,6 +33,8 @@ import telegram IGNORED_OBJECTS = ('ResponseParameters', 'CallbackGame') IGNORED_PARAMETERS = {'self', 'args', 'kwargs', 'read_latency', 'network_delay', 'timeout', 'bot', 'new_chat_member'} + + # TODO: New_chat_member is still in our lib but already removed from TG's docs. @@ -49,10 +51,10 @@ def parse_table(h4): if not table: return [] head = [td.text for td in table.tr.find_all('td')] - row = namedtuple('{}TableRow'.format(h4.text), ','.join(head)) + # row = namedtuple('{}TableRow'.format(h4.text), ','.join(head)) t = [] for tr in table.find_all('tr')[1:]: - t.append(row(*[td.text for td in tr.find_all('td')])) + t.append([td.text for td in tr.find_all('td')]) return t @@ -66,12 +68,12 @@ def check_method(h4): checked = [] for parameter in table: - param = sig.parameters.get(parameter.Parameters) - assert param is not None, "Parameter {} not found in {}".format(parameter.Parameters, + param = sig.parameters.get(parameter[0]) + assert param is not None, "Parameter {} not found in {}".format(parameter[0], method.__name__) # TODO: Check type via docstring # TODO: Check if optional or required - checked.append(parameter.Parameters) + checked.append(parameter[0]) ignored = IGNORED_PARAMETERS.copy() if name == 'getUpdates': @@ -82,7 +84,7 @@ def check_method(h4): ignored |= {'edit_message'} # TODO: Now deprecated, so no longer in telegrams docs elif name == 'sendContact': ignored |= {'contact'} # Added for ease of use - elif name == 'sendLocation': + elif name in ['sendLocation', 'editMessageLiveLocation']: ignored |= {'location'} # Added for ease of use elif name == 'sendVenue': ignored |= {'venue'} # Added for ease of use @@ -100,7 +102,7 @@ def check_object(h4): checked = [] for parameter in table: - field = parameter.Field + field = parameter[0] if field == 'from': field = 'from_user' elif name.startswith('InlineQueryResult') and field == 'type':