support 3.4 API (#865)

This commit is contained in:
Eldinnie 2017-10-14 20:03:02 +02:00 committed by Noam Meltzer
parent 8a8b1215c8
commit bfad2fa1f3
13 changed files with 403 additions and 35 deletions

View file

@ -96,7 +96,7 @@ make the development of bots easy and straightforward. These classes are contain
Telegram API support 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 Installing

View file

@ -768,6 +768,7 @@ class Bot(TelegramObject):
reply_markup=None, reply_markup=None,
timeout=None, timeout=None,
location=None, location=None,
live_period=None,
**kwargs): **kwargs):
"""Use this method to send point on the map. """Use this method to send point on the map.
@ -780,6 +781,8 @@ class Bot(TelegramObject):
latitude (:obj:`float`, optional): Latitude of location. latitude (:obj:`float`, optional): Latitude of location.
longitude (:obj:`float`, optional): Longitude of location. longitude (:obj:`float`, optional): Longitude of location.
location (:class:`telegram.Location`, optional): The location to send. 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 disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound. receive a notification with no sound.
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the 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): if not (all([latitude, longitude]) or location):
raise ValueError("Either location or latitude and longitude must be passed as" 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): if isinstance(location, Location):
latitude = location.latitude latitude = location.latitude
@ -811,6 +818,114 @@ class Bot(TelegramObject):
data = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude} 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 return url, data
@log @log
@ -1825,6 +1940,63 @@ class Bot(TelegramObject):
return ChatMember.de_json(result, self) 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): def get_webhook_info(self, timeout=None, **kwargs):
"""Use this method to get current webhook status. Requires no parameters. """Use this method to get current webhook status. Requires no parameters.
@ -2794,6 +2966,8 @@ class Bot(TelegramObject):
sendVoice = send_voice sendVoice = send_voice
sendVideoNote = send_video_note sendVideoNote = send_video_note
sendLocation = send_location sendLocation = send_location
editMessageLiveLocation = edit_message_live_location
stopMessageLiveLocation = stop_message_live_location
sendVenue = send_venue sendVenue = send_venue
sendContact = send_contact sendContact = send_contact
sendGame = send_game sendGame = send_game
@ -2814,6 +2988,8 @@ class Bot(TelegramObject):
getChat = get_chat getChat = get_chat
getChatAdministrators = get_chat_administrators getChatAdministrators = get_chat_administrators
getChatMember = get_chat_member getChatMember = get_chat_member
setChatStickerSet = set_chat_sticker_set
deleteChatStickerSet = delete_chat_sticker_set
getChatMembersCount = get_chat_members_count getChatMembersCount = get_chat_members_count
getWebhookInfo = get_webhook_info getWebhookInfo = get_webhook_info
setGameScore = set_game_score setGameScore = set_game_score

View file

@ -38,6 +38,9 @@ class Chat(TelegramObject):
invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats. invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats.
pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups. pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups.
Returned only in get_chat. 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: Args:
id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits 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. pinned_message (:class:`telegram.Message`, optional): Pinned message, for supergroups.
Returned only in get_chat. Returned only in get_chat.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments.
""" """
@ -87,6 +94,8 @@ class Chat(TelegramObject):
description=None, description=None,
invite_link=None, invite_link=None,
pinned_message=None, pinned_message=None,
sticker_set_name=None,
can_set_sticker_set=None,
**kwargs): **kwargs):
# Required # Required
self.id = int(id) self.id = int(id)
@ -101,6 +110,8 @@ class Chat(TelegramObject):
self.description = description self.description = description
self.invite_link = invite_link self.invite_link = invite_link
self.pinned_message = pinned_message 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.bot = bot
self._id_attrs = (self.id,) self._id_attrs = (self.id,)

View file

@ -33,6 +33,8 @@ class InlineQueryResultLocation(InlineQueryResult):
latitude (:obj:`float`): Location latitude in degrees. latitude (:obj:`float`): Location latitude in degrees.
longitude (:obj:`float`): Location longitude in degrees. longitude (:obj:`float`): Location longitude in degrees.
title (:obj:`str`): Location title. 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 reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message. to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
@ -46,6 +48,8 @@ class InlineQueryResultLocation(InlineQueryResult):
latitude (:obj:`float`): Location latitude in degrees. latitude (:obj:`float`): Location latitude in degrees.
longitude (:obj:`float`): Location longitude in degrees. longitude (:obj:`float`): Location longitude in degrees.
title (:obj:`str`): Location title. 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 reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message. to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
@ -62,6 +66,7 @@ class InlineQueryResultLocation(InlineQueryResult):
latitude, latitude,
longitude, longitude,
title, title,
live_period=None,
reply_markup=None, reply_markup=None,
input_message_content=None, input_message_content=None,
thumb_url=None, thumb_url=None,
@ -75,6 +80,8 @@ class InlineQueryResultLocation(InlineQueryResult):
self.title = title self.title = title
# Optionals # Optionals
if live_period:
self.live_period = live_period
if reply_markup: if reply_markup:
self.reply_markup = reply_markup self.reply_markup = reply_markup
if input_message_content: if input_message_content:

View file

@ -32,11 +32,14 @@ class InputLocationMessageContent(InputMessageContent):
Args: Args:
latitude (:obj:`float`): Latitude of the location in degrees. latitude (:obj:`float`): Latitude of the location in degrees.
longitude (:obj:`float`): Longitude 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments.
""" """
def __init__(self, latitude, longitude, **kwargs): def __init__(self, latitude, longitude, live_period=None, **kwargs):
# Required # Required
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
self.live_period = live_period

View file

@ -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.deprecate import warn_deprecate_obj
from telegram.utils.helpers import escape_html, escape_markdown, to_timestamp, from_timestamp from telegram.utils.helpers import escape_html, escape_markdown, to_timestamp, from_timestamp
_UNDEFINED = object() _UNDEFINED = object()
@ -54,6 +53,10 @@ class Message(TelegramObject):
usernames, URLs, bot commands, etc. that appear in the text. See usernames, URLs, bot commands, etc. that appear in the text. See
:attr:`Message.parse_entity` and :attr:`parse_entities` methods for how to use :attr:`Message.parse_entity` and :attr:`parse_entities` methods for how to use
properly. 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. audio (:class:`telegram.Audio`): Optional. Information about the file.
document (:class:`telegram.Document`): Optional. Information about the file. document (:class:`telegram.Document`): Optional. Information about the file.
game (:class:`telegram.Game`): Optional. Information about the game. 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 (List[:class:`telegram.MessageEntity`], optional): For text messages, special
entities like usernames, URLs, bot commands, etc. that appear in the text. See 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. 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 audio (:class:`telegram.Audio`, optional): Message is an audio file, information
about the file. about the file.
document (:class:`telegram.Document`, optional): Message is a general file, information document (:class:`telegram.Document`, optional): Message is a general file, information
@ -196,6 +203,7 @@ class Message(TelegramObject):
edit_date=None, edit_date=None,
text=None, text=None,
entities=None, entities=None,
caption_entities=None,
audio=None, audio=None,
document=None, document=None,
game=None, game=None,
@ -239,6 +247,7 @@ class Message(TelegramObject):
self.edit_date = edit_date self.edit_date = edit_date
self.text = text self.text = text
self.entities = entities or list() self.entities = entities or list()
self.caption_entities = caption_entities or list()
self.audio = audio self.audio = audio
self.game = game self.game = game
self.document = document self.document = document
@ -289,6 +298,7 @@ class Message(TelegramObject):
data['date'] = from_timestamp(data['date']) data['date'] = from_timestamp(data['date'])
data['chat'] = Chat.de_json(data.get('chat'), bot) data['chat'] = Chat.de_json(data.get('chat'), bot)
data['entities'] = MessageEntity.de_list(data.get('entities'), 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'] = User.de_json(data.get('forward_from'), bot)
data['forward_from_chat'] = Chat.de_json(data.get('forward_from_chat'), bot) data['forward_from_chat'] = Chat.de_json(data.get('forward_from_chat'), bot)
data['forward_date'] = from_timestamp(data.get('forward_date')) 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] data['photo'] = [p.to_dict() for p in self.photo]
if self.entities: if self.entities:
data['entities'] = [e.to_dict() for e in 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: if self.new_chat_photo:
data['new_chat_photo'] = [p.to_dict() for p in 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) 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. be an entity that belongs to this message.
Returns: 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 # 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') 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): def parse_entities(self, types=None):
""" """
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. 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 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): def _text_html(self, urled=False):
entities = self.parse_entities() entities = self.parse_entities()
message_text = self.text message_text = self.text

View file

@ -107,15 +107,6 @@ class TestBot(object):
# send_photo, send_audio, send_document, send_sticker, send_video, send_voice # 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. # 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) @flaky(3, 1)
@pytest.mark.timeout(10) @pytest.mark.timeout(10)
def test_send_venue(self, bot, chat_id): def test_send_venue(self, bot, chat_id):
@ -368,6 +359,14 @@ class TestBot(object):
assert chat_member.status == 'administrator' assert chat_member.status == 'administrator'
assert chat_member.user.username == 'EchteEldin' 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)') @pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (yet)')
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.timeout(10) @pytest.mark.timeout(10)

View file

@ -27,7 +27,8 @@ from telegram import User
def chat(bot): def chat(bot):
return Chat(TestChat.id, TestChat.title, TestChat.type, return Chat(TestChat.id, TestChat.title, TestChat.type,
all_members_are_administrators=TestChat.all_members_are_administrators, 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): class TestChat(object):
@ -35,13 +36,17 @@ class TestChat(object):
title = 'ToledosPalaceBot - Group' title = 'ToledosPalaceBot - Group'
type = 'group' type = 'group'
all_members_are_administrators = False all_members_are_administrators = False
sticker_set_name = 'stickers'
can_set_sticker_set = False
def test_de_json(self, bot): def test_de_json(self, bot):
json_dict = { json_dict = {
'id': TestChat.id, 'id': self.id,
'title': TestChat.title, 'title': self.title,
'type': TestChat.type, 'type': self.type,
'all_members_are_administrators': TestChat.all_members_are_administrators '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) chat = Chat.de_json(json_dict, bot)
@ -49,6 +54,8 @@ class TestChat(object):
assert chat.title == self.title assert chat.title == self.title
assert chat.type == self.type assert chat.type == self.type
assert chat.all_members_are_administrators == self.all_members_are_administrators 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): def test_to_dict(self, chat):
chat_dict = chat.to_dict() chat_dict = chat.to_dict()

View file

@ -29,6 +29,7 @@ def inline_query_result_location():
TestInlineQueryResultLocation.latitude, TestInlineQueryResultLocation.latitude,
TestInlineQueryResultLocation.longitude, TestInlineQueryResultLocation.longitude,
TestInlineQueryResultLocation.title, TestInlineQueryResultLocation.title,
live_period=TestInlineQueryResultLocation.live_period,
thumb_url=TestInlineQueryResultLocation.thumb_url, thumb_url=TestInlineQueryResultLocation.thumb_url,
thumb_width=TestInlineQueryResultLocation.thumb_width, thumb_width=TestInlineQueryResultLocation.thumb_width,
thumb_height=TestInlineQueryResultLocation.thumb_height, thumb_height=TestInlineQueryResultLocation.thumb_height,
@ -42,6 +43,7 @@ class TestInlineQueryResultLocation(object):
latitude = 0.0 latitude = 0.0
longitude = 1.0 longitude = 1.0
title = 'title' title = 'title'
live_period = 70
thumb_url = 'thumb url' thumb_url = 'thumb url'
thumb_width = 10 thumb_width = 10
thumb_height = 15 thumb_height = 15
@ -54,6 +56,7 @@ class TestInlineQueryResultLocation(object):
assert inline_query_result_location.latitude == self.latitude assert inline_query_result_location.latitude == self.latitude
assert inline_query_result_location.longitude == self.longitude assert inline_query_result_location.longitude == self.longitude
assert inline_query_result_location.title == self.title 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_url == self.thumb_url
assert inline_query_result_location.thumb_width == self.thumb_width assert inline_query_result_location.thumb_width == self.thumb_width
assert inline_query_result_location.thumb_height == self.thumb_height assert inline_query_result_location.thumb_height == self.thumb_height
@ -72,6 +75,8 @@ class TestInlineQueryResultLocation(object):
assert inline_query_result_location_dict['longitude'] == \ assert inline_query_result_location_dict['longitude'] == \
inline_query_result_location.longitude inline_query_result_location.longitude
assert inline_query_result_location_dict['title'] == inline_query_result_location.title 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'] == \ assert inline_query_result_location_dict['thumb_url'] == \
inline_query_result_location.thumb_url inline_query_result_location.thumb_url
assert inline_query_result_location_dict['thumb_width'] == \ assert inline_query_result_location_dict['thumb_width'] == \

View file

@ -25,16 +25,19 @@ from telegram import InputLocationMessageContent
@pytest.fixture(scope='class') @pytest.fixture(scope='class')
def input_location_message_content(): def input_location_message_content():
return InputLocationMessageContent(TestInputLocationMessageContent.latitude, return InputLocationMessageContent(TestInputLocationMessageContent.latitude,
TestInputLocationMessageContent.longitude) TestInputLocationMessageContent.longitude,
live_period=TestInputLocationMessageContent.live_period)
class TestInputLocationMessageContent(object): class TestInputLocationMessageContent(object):
latitude = -23.691288 latitude = -23.691288
longitude = -46.788279 longitude = -46.788279
live_period = 80
def test_expected_values(self, input_location_message_content): def test_expected_values(self, input_location_message_content):
assert input_location_message_content.longitude == self.longitude assert input_location_message_content.longitude == self.longitude
assert input_location_message_content.latitude == self.latitude 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): def test_to_dict(self, input_location_message_content):
input_location_message_content_dict = input_location_message_content.to_dict() input_location_message_content_dict = input_location_message_content.to_dict()
@ -44,3 +47,5 @@ class TestInputLocationMessageContent(object):
input_location_message_content.latitude input_location_message_content.latitude
assert input_location_message_content_dict['longitude'] == \ assert input_location_message_content_dict['longitude'] == \
input_location_message_content.longitude input_location_message_content.longitude
assert input_location_message_content_dict[
'live_period'] == input_location_message_content.live_period

View file

@ -18,8 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/]. # along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest import pytest
from flaky import flaky
from telegram import Location from telegram import Location
from telegram.error import BadRequest
@pytest.fixture(scope='class') @pytest.fixture(scope='class')
@ -39,6 +41,46 @@ class TestLocation(object):
assert location.latitude == self.latitude assert location.latitude == self.latitude
assert location.longitude == self.longitude 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_send_with_location(self, monkeypatch, bot, chat_id, location):
def test(_, url, data, **kwargs): def test(_, url, data, **kwargs):
lat = data['latitude'] == location.latitude lat = data['latitude'] == location.latitude
@ -48,10 +90,32 @@ class TestLocation(object):
monkeypatch.setattr('telegram.utils.request.Request.post', test) monkeypatch.setattr('telegram.utils.request.Request.post', test)
assert bot.send_location(location=location, chat_id=chat_id) 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): def test_send_location_without_required(self, bot, chat_id):
with pytest.raises(ValueError, match='Either location or latitude and longitude'): with pytest.raises(ValueError, match='Either location or latitude and longitude'):
bot.send_location(chat_id=chat_id) 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): def test_to_dict(self, location):
location_dict = location.to_dict() location_dict = location.to_dict()

View file

@ -40,9 +40,12 @@ def message(bot):
'forward_date': datetime.now()}, 'forward_date': datetime.now()},
{'reply_to_message': Message(50, None, None, None)}, {'reply_to_message': Message(50, None, None, None)},
{'edit_date': datetime.now()}, {'edit_date': datetime.now()},
{'test': 'a text message', {'text': 'a text message',
'enitites': [MessageEntity('bold', 10, 4), 'enitites': [MessageEntity('bold', 10, 4),
MessageEntity('italic', 16, 7)]}, MessageEntity('italic', 16, 7)]},
{'caption': 'A message caption',
'caption_entities': [MessageEntity('bold', 1, 1),
MessageEntity('text_link', 4, 3)]},
{'audio': Audio('audio_id', 12), {'audio': Audio('audio_id', 12),
'caption': 'audio_file'}, 'caption': 'audio_file'},
{'document': Document('document_id'), {'document': Document('document_id'),
@ -78,12 +81,13 @@ def message(bot):
{'forward_signature': 'some_forward_sign'}, {'forward_signature': 'some_forward_sign'},
{'author_signature': 'some_author_sign'} {'author_signature': 'some_author_sign'}
], ],
ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text', 'audio', ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text',
'document', 'game', 'photo', 'sticker', 'video', 'voice', 'video_note', 'caption_entities', 'audio', 'document', 'game', 'photo', 'sticker', 'video',
'new_members', 'contact', 'location', 'venue', 'left_member', 'new_title', 'voice', 'video_note', 'new_members', 'contact', 'location', 'venue',
'new_photo', 'delete_photo', 'group_created', 'supergroup_created', 'left_member', 'new_title', 'new_photo', 'delete_photo', 'group_created',
'channel_created', 'migrated_to', 'migrated_from', 'pinned', 'invoice', 'supergroup_created', 'channel_created', 'migrated_to', 'migrated_from',
'successful_payment', 'forward_signature', 'author_signature']) 'pinned', 'invoice', 'successful_payment', 'forward_signature',
'author_signature'])
def message_params(bot, request): def message_params(bot, request):
return Message(message_id=TestMessage.id, return Message(message_id=TestMessage.id,
from_user=TestMessage.from_user, 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]) message = Message(1, self.from_user, self.date, self.chat, text=text, entities=[entity])
assert message.parse_entity(entity) == 'http://google.com' 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): def test_parse_entities(self):
text = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467' text = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467'
b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape') 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(MessageEntity.URL) == {entity: 'http://google.com'}
assert message.parse_entities() == {entity: 'http://google.com', entity_2: 'h'} 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): def test_text_html_simple(self):
test_html_string = ('Test for &lt;<b>bold</b>, <i>ita_lic</i>, <code>code</code>, ' test_html_string = ('Test for &lt;<b>bold</b>, <i>ita_lic</i>, <code>code</code>, '
'<a href="http://github.com/">links</a> and <pre>pre</pre>. ' '<a href="http://github.com/">links</a> and <pre>pre</pre>. '
@ -201,7 +223,6 @@ class TestMessage(object):
item = None item = None
assert message_params.effective_attachment == item assert message_params.effective_attachment == item
def test_reply_text(self, monkeypatch, message): def test_reply_text(self, monkeypatch, message):
def test(*args, **kwargs): def test(*args, **kwargs):
id = args[1] == message.chat_id id = args[1] == message.chat_id

View file

@ -33,6 +33,8 @@ import telegram
IGNORED_OBJECTS = ('ResponseParameters', 'CallbackGame') IGNORED_OBJECTS = ('ResponseParameters', 'CallbackGame')
IGNORED_PARAMETERS = {'self', 'args', 'kwargs', 'read_latency', 'network_delay', 'timeout', 'bot', IGNORED_PARAMETERS = {'self', 'args', 'kwargs', 'read_latency', 'network_delay', 'timeout', 'bot',
'new_chat_member'} 'new_chat_member'}
# TODO: New_chat_member is still in our lib but already removed from TG's docs. # 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: if not table:
return [] return []
head = [td.text for td in table.tr.find_all('td')] 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 = [] t = []
for tr in table.find_all('tr')[1:]: 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 return t
@ -66,12 +68,12 @@ def check_method(h4):
checked = [] checked = []
for parameter in table: for parameter in table:
param = sig.parameters.get(parameter.Parameters) param = sig.parameters.get(parameter[0])
assert param is not None, "Parameter {} not found in {}".format(parameter.Parameters, assert param is not None, "Parameter {} not found in {}".format(parameter[0],
method.__name__) method.__name__)
# TODO: Check type via docstring # TODO: Check type via docstring
# TODO: Check if optional or required # TODO: Check if optional or required
checked.append(parameter.Parameters) checked.append(parameter[0])
ignored = IGNORED_PARAMETERS.copy() ignored = IGNORED_PARAMETERS.copy()
if name == 'getUpdates': if name == 'getUpdates':
@ -82,7 +84,7 @@ def check_method(h4):
ignored |= {'edit_message'} # TODO: Now deprecated, so no longer in telegrams docs ignored |= {'edit_message'} # TODO: Now deprecated, so no longer in telegrams docs
elif name == 'sendContact': elif name == 'sendContact':
ignored |= {'contact'} # Added for ease of use ignored |= {'contact'} # Added for ease of use
elif name == 'sendLocation': elif name in ['sendLocation', 'editMessageLiveLocation']:
ignored |= {'location'} # Added for ease of use ignored |= {'location'} # Added for ease of use
elif name == 'sendVenue': elif name == 'sendVenue':
ignored |= {'venue'} # Added for ease of use ignored |= {'venue'} # Added for ease of use
@ -100,7 +102,7 @@ def check_object(h4):
checked = [] checked = []
for parameter in table: for parameter in table:
field = parameter.Field field = parameter[0]
if field == 'from': if field == 'from':
field = 'from_user' field = 'from_user'
elif name.startswith('InlineQueryResult') and field == 'type': elif name.startswith('InlineQueryResult') and field == 'type':