From cd9211879771ea3213beb8a4070a4b0022968fd2 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 24 Apr 2020 16:33:31 +0200 Subject: [PATCH 1/6] API 4.8 --- README.rst | 2 +- telegram/bot.py | 38 +++++++++++++++ telegram/dice.py | 13 ++++-- telegram/ext/filters.py | 64 +++++++++++++++++-------- telegram/poll.py | 101 ++++++++++++++++++++++++++++++++++++++-- tests/test_bot.py | 73 +++++++++++++++++++++++++++-- tests/test_dice.py | 14 ++++-- tests/test_filters.py | 23 +++++++-- tests/test_message.py | 2 +- tests/test_poll.py | 44 +++++++++++++++-- 10 files changed, 330 insertions(+), 44 deletions(-) diff --git a/README.rst b/README.rst index 27f2113e979..352fc8a6926 100644 --- a/README.rst +++ b/README.rst @@ -93,7 +93,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -All types and methods of the Telegram Bot API **4.7** are supported. +All types and methods of the Telegram Bot API **4.8** are supported. ========== Installing diff --git a/telegram/bot.py b/telegram/bot.py index 253f2cd7b0f..12db5d87431 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -3633,6 +3633,10 @@ def send_poll(self, reply_to_message_id=None, reply_markup=None, timeout=None, + explanation=None, + explanation_parse_mode=DEFAULT_NONE, + open_period=None, + close_date=None, **kwargs): """ Use this method to send a native poll. @@ -3650,6 +3654,18 @@ def send_poll(self, answers, ignored for polls in quiz mode, defaults to False. correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer option, required for polls in quiz mode. + explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect + answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most + 2 line feeds after entities parsing. + explanation_parse_mode (:obj:`str`, optional): Mode for parsing entities in the + explanation. See the constants in :class:`telegram.ParseMode` for the available + modes. + open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active + after creation, 5-600. Can't be used together with :attr:`close_date`. + close_date (:obj:`int` | :obj:`datetime.datetime`, optional): Point in time (Unix + timestamp) when the poll will be automatically closed. Must be at least 5 and no + more than 600 seconds in the future. Can't be used together with + :attr:`open_period`. is_closed (:obj:`bool`, optional): Pass True, if the poll needs to be immediately closed. This can be useful for poll preview. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will @@ -3679,6 +3695,12 @@ def send_poll(self, 'options': options } + if explanation_parse_mode == DEFAULT_NONE: + if self.defaults: + explanation_parse_mode = self.defaults.parse_mode + else: + explanation_parse_mode = None + if not is_anonymous: data['is_anonymous'] = is_anonymous if type: @@ -3689,6 +3711,16 @@ def send_poll(self, data['correct_option_id'] = correct_option_id if is_closed: data['is_closed'] = is_closed + if explanation: + data['explanation'] = explanation + if explanation_parse_mode: + data['explanation_parse_mode'] = explanation_parse_mode + if open_period: + data['open_period'] = open_period + if close_date: + if isinstance(close_date, datetime): + close_date = to_timestamp(close_date) + data['close_date'] = close_date return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, @@ -3749,6 +3781,7 @@ def send_dice(self, reply_to_message_id=None, reply_markup=None, timeout=None, + emoji=None, **kwargs): """ Use this method to send a dice, which will have a random value from 1 to 6. On success, the @@ -3756,6 +3789,8 @@ def send_dice(self, Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat. + emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based. + Currently, must be one of “🎲” or “🎯”. Defauts to “🎲” 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 @@ -3781,6 +3816,9 @@ def send_dice(self, 'chat_id': chat_id, } + if emoji: + data['emoji'] = emoji + return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) diff --git a/telegram/dice.py b/telegram/dice.py index b90aeb36320..e803fbfa9bd 100644 --- a/telegram/dice.py +++ b/telegram/dice.py @@ -23,17 +23,21 @@ class Dice(TelegramObject): """ - This object represents a dice with random value from 1 to 6. (The singular form of "dice" is - "die". However, PTB mimics the Telegram API, which uses the term "dice".) + This object represents a dice with random value from 1 to 6 for currently supported base eomji. + (The singular form of "dice" is "die". However, PTB mimics the Telegram API, which uses the + term "dice".) Attributes: value (:obj:`int`): Value of the dice. + emoji (:obj:`str`): Emoji on which the dice throw animation is based. Args: value (:obj:`int`): Value of the dice, 1-6. + emoji (:obj:`str`): Emoji on which the dice throw animation is based. """ - def __init__(self, value, **kwargs): + def __init__(self, value, emoji, **kwargs): self.value = value + self.emoji = emoji @classmethod def de_json(cls, data, bot): @@ -41,3 +45,6 @@ def de_json(cls, data, bot): return None return cls(**data) + + ALL_EMOJI = ['🎲', '🎯'] + """List[:obj:`str`]: List of all supported base emoji.""" diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index ea4e274226c..0f96fa6adec 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -215,6 +215,41 @@ def __repr__(self): self.and_filter or self.or_filter) +class _DiceEmoji(BaseFilter): + + def __init__(self, emoji=None, name=None): + self.name = 'Filters.dice.{}'.format(name) if name else 'Filters.dice' + self.emoji = emoji + + class _DiceValues(BaseFilter): + + def __init__(self, values, emoji=None, name=None): + self.values = [values] if isinstance(values, int) else values + self.emoji = emoji + if name: + self.name = 'Filters.dice.{}({})'.format(name, values) + else: + self.name = 'Filters.dice({})'.format(values) + + def filter(self, message): + if bool(message.dice and message.dice.value in self.values): + if self.emoji: + return message.dice.emoji == self.emoji + return True + + def __call__(self, update): + if isinstance(update, Update): + return self.filter(update.effective_message) + else: + return self._DiceValues(update, emoji=self.emoji, name=self.name) + + def filter(self, message): + if bool(message.dice): + if self.emoji: + return message.dice.emoji == self.emoji + return True + + class Filters(object): """Predefined filters for use as the `filter` argument of :class:`telegram.ext.MessageHandler`. @@ -967,26 +1002,9 @@ def filter(self, message): poll = _Poll() """Messages that contain a :class:`telegram.Poll`.""" - class _Dice(BaseFilter): - name = 'Filters.dice' - - class _DiceValues(BaseFilter): - - def __init__(self, values): - self.values = [values] if isinstance(values, int) else values - self.name = 'Filters.dice({})'.format(values) - - def filter(self, message): - return bool(message.dice and message.dice.value in self.values) - - def __call__(self, update): - if isinstance(update, Update): - return self.filter(update.effective_message) - else: - return self._DiceValues(update) - - def filter(self, message): - return bool(message.dice) + class _Dice(_DiceEmoji): + dice = _DiceEmoji('🎲', 'dice') + darts = _DiceEmoji('🎯', 'darts') dice = _Dice() """Dice Messages. If an integer or a list of integers is passed, it filters messages to only @@ -1007,6 +1025,12 @@ def filter(self, message): Note: Dice messages don't have text. If you want to filter either text or dice messages, use ``Filters.text | Filters.dice``. + + Attributes: + dice: Dice messages with the emoji 🎲. Passing a list of integers is supported just as for + :attr:`Filters.dice`. + darts: Dice messages with the emoji 🎯. Passing a list of integers is supported just as for + :attr:`Filters.dice`. """ class language(BaseFilter): diff --git a/telegram/poll.py b/telegram/poll.py index d2b116ee399..f92224837ef 100644 --- a/telegram/poll.py +++ b/telegram/poll.py @@ -19,7 +19,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Poll.""" -from telegram import (TelegramObject, User) +import sys + +from telegram import (TelegramObject, User, MessageEntity) +from telegram.utils.helpers import to_timestamp, from_timestamp class PollOption(TelegramObject): @@ -95,6 +98,14 @@ class Poll(TelegramObject): type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`. allows_multiple_answers (:obj:`bool`): True, if the poll allows multiple answers. correct_option_id (:obj:`int`): Optional. Identifier of the correct answer option. + explanation (:obj:`str`): Optional. Text that is shown when a user chooses an incorrect + answer or taps on the lamp icon in a quiz-style poll. + explanation_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities + like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`. + open_period (:obj:`int`): Optional. Amount of time in seconds the poll will be active + after creation. + close_date (:obj:`datetime.datetime`): Optional. Point in time when the poll will be + automatically closed. Args: id (:obj:`str`): Unique poll identifier. @@ -107,11 +118,32 @@ class Poll(TelegramObject): correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer option. Available only for polls in the quiz mode, which are closed, or was sent (not forwarded) by the bot or to the private chat with the bot. + explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect + answer or taps on the lamp icon in a quiz-style poll, 0-200 characters. + explanation_entities (List[:class:`telegram.MessageEntity`], optional): Special entities + like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`. + open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active + after creation. + close_date (:obj:`datetime.datetime`, optional): Point in time (Unix timestamp) when the + poll will be automatically closed. Converted to :obj:`datetime.datetime`. """ - def __init__(self, id, question, options, total_voter_count, is_closed, is_anonymous, type, - allows_multiple_answers, correct_option_id=None, **kwargs): + def __init__(self, + id, + question, + options, + total_voter_count, + is_closed, + is_anonymous, + type, + allows_multiple_answers, + correct_option_id=None, + explanation=None, + explanation_entities=None, + open_period=None, + close_date=None, + **kwargs): self.id = id self.question = question self.options = options @@ -121,6 +153,10 @@ def __init__(self, id, question, options, total_voter_count, is_closed, is_anony self.type = type self.allows_multiple_answers = allows_multiple_answers self.correct_option_id = correct_option_id + self.explanation = explanation + self.explanation_entities = explanation_entities + self.open_period = open_period + self.close_date = close_date self._id_attrs = (self.id,) @@ -132,6 +168,8 @@ def de_json(cls, data, bot): data = super(Poll, cls).de_json(data, bot) data['options'] = [PollOption.de_json(option, bot) for option in data['options']] + data['explanation_entities'] = MessageEntity.de_list(data.get('explanation_entities'), bot) + data['close_date'] = from_timestamp(data.get('close_date')) return cls(**data) @@ -139,9 +177,66 @@ def to_dict(self): data = super(Poll, self).to_dict() data['options'] = [x.to_dict() for x in self.options] + if self.explanation_entities: + data['explanation_entities'] = [e.to_dict() for e in self.explanation_entities] + data['close_date'] = to_timestamp(data.get('close_date')) return data + def parse_explanation_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.text`` 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.explanation[entity.offset:entity.offset + entity.length] + else: + entity_text = self.explanation.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_explanation_entities(self, types=None): + """ + Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. + It contains entities from this polls explanation filtered by their ``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:`explanation_entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_explanation_entity` for more info. + + Args: + types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the + ``type`` attribute of an entity is contained in this list, it will be returned. + Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`. + + 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_explanation_entity(entity) + for entity in self.explanation_entities if entity.type in types + } + REGULAR = "regular" """:obj:`str`: 'regular'""" QUIZ = "quiz" diff --git a/tests/test_bot.py b/tests/test_bot.py index ee81ed35c95..9459eb252c0 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -27,7 +27,7 @@ from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboardMarkup, InlineKeyboardButton, InlineQueryResultArticle, InputTextMessageContent, ShippingOption, LabeledPrice, ChatPermissions, Poll, BotCommand, - InlineQueryResultDocument) + InlineQueryResultDocument, Dice, MessageEntity, ParseMode) from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter from telegram.utils.helpers import from_timestamp, escape_markdown @@ -213,18 +213,83 @@ def test_send_and_stop_poll(self, bot, super_group_id, reply_markup): assert poll.question == question assert poll.total_voter_count == 0 + explanation = '[Here is a link](https://google.com)' + explanation_entities = [ + MessageEntity(MessageEntity.TEXT_LINK, 0, 14, url='https://google.com') + ] message_quiz = bot.send_poll(chat_id=super_group_id, question=question, options=answers, - type=Poll.QUIZ, correct_option_id=2, is_closed=True) + type=Poll.QUIZ, correct_option_id=2, is_closed=True, + explanation=explanation, + explanation_parse_mode=ParseMode.MARKDOWN_V2) assert message_quiz.poll.correct_option_id == 2 assert message_quiz.poll.type == Poll.QUIZ assert message_quiz.poll.is_closed + assert message_quiz.poll.explanation == 'Here is a link' + assert message_quiz.poll.explanation_entities == explanation_entities + + @flaky(3, 1) + @pytest.mark.timeout(10) + @pytest.mark.parametrize(['open_period', 'close_date'], [(5, None), (None, True)]) + def test_send_open_period(self, bot, super_group_id, open_period, close_date): + question = 'Is this a test?' + answers = ['Yes', 'No', 'Maybe'] + reply_markup = InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text='text', callback_data='data')) + + if close_date: + close_date = dtm.datetime.utcnow() + dtm.timedelta(seconds=5) + + message = bot.send_poll(chat_id=super_group_id, question=question, options=answers, + is_anonymous=False, allows_multiple_answers=True, timeout=60, + open_period=open_period, close_date=close_date) + time.sleep(5.1) + new_message = bot.edit_message_reply_markup(chat_id=super_group_id, + message_id=message.message_id, + reply_markup=reply_markup, timeout=60) + assert new_message.poll.id == message.poll.id + assert new_message.poll.is_closed + + @flaky(3, 1) + @pytest.mark.timeout(10) + @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) + def test_send_poll_default_parse_mode(self, default_bot, super_group_id): + explanation = 'Italic Bold Code' + explanation_markdown = '_Italic_ *Bold* `Code`' + question = 'Is this a test?' + answers = ['Yes', 'No', 'Maybe'] + + message = default_bot.send_poll(chat_id=super_group_id, question=question, options=answers, + type=Poll.QUIZ, correct_option_id=2, is_closed=True, + explanation=explanation_markdown) + assert message.poll.explanation == explanation + assert message.poll.explanation_entities == [ + MessageEntity(MessageEntity.ITALIC, 0, 6), + MessageEntity(MessageEntity.BOLD, 7, 4), + MessageEntity(MessageEntity.CODE, 12, 4) + ] + + message = default_bot.send_poll(chat_id=super_group_id, question=question, options=answers, + type=Poll.QUIZ, correct_option_id=2, is_closed=True, + explanation=explanation_markdown, + explanation_parse_mode=None) + assert message.poll.explanation == explanation_markdown + assert message.poll.explanation_entities == [] + + message = default_bot.send_poll(chat_id=super_group_id, question=question, options=answers, + type=Poll.QUIZ, correct_option_id=2, is_closed=True, + explanation=explanation_markdown, + explanation_parse_mode='HTML') + assert message.poll.explanation == explanation_markdown + assert message.poll.explanation_entities == [] @flaky(3, 1) @pytest.mark.timeout(10) - def test_send_dice(self, bot, chat_id): - message = bot.send_dice(chat_id) + @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI) + def test_send_dice(self, bot, chat_id, emoji): + message = bot.send_dice(chat_id, emoji=emoji) assert message.dice + assert message.dice.emoji == emoji @flaky(3, 1) @pytest.mark.timeout(10) diff --git a/tests/test_dice.py b/tests/test_dice.py index 89805f45fb5..1770715d828 100644 --- a/tests/test_dice.py +++ b/tests/test_dice.py @@ -22,19 +22,22 @@ from telegram import Dice -@pytest.fixture(scope="class") -def dice(): - return Dice(value=5) +@pytest.fixture(scope="class", + params=Dice.ALL_EMOJI) +def dice(request): + return Dice(value=5, emoji=request.param) class TestDice(object): value = 4 - def test_de_json(self, bot): - json_dict = {'value': self.value} + @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI) + def test_de_json(self, bot, emoji): + json_dict = {'value': self.value, 'emoji': emoji} dice = Dice.de_json(json_dict, bot) assert dice.value == self.value + assert dice.emoji == emoji assert Dice.de_json(None, bot) is None def test_to_dict(self, dice): @@ -42,3 +45,4 @@ def test_to_dict(self, dice): assert isinstance(dice_dict, dict) assert dice_dict['value'] == dice.value + assert dice_dict['emoji'] == dice.emoji diff --git a/tests/test_filters.py b/tests/test_filters.py index c8961589938..d2dc50d2e91 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -622,22 +622,37 @@ def test_filters_poll(self, update): update.message.poll = 'test' assert Filters.poll(update) - def test_filters_dice(self, update): - update.message.dice = Dice(4) + @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI) + def test_filters_dice(self, update, emoji): + update.message.dice = Dice(4, emoji) assert Filters.dice(update) update.message.dice = None assert not Filters.dice(update) - def test_filters_dice_iterable(self, update): + @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI) + def test_filters_dice_list(self, update, emoji): update.message.dice = None assert not Filters.dice(5)(update) - update.message.dice = Dice(5) + update.message.dice = Dice(5, emoji) assert Filters.dice(5)(update) assert Filters.dice({5, 6})(update) assert not Filters.dice(1)(update) assert not Filters.dice([2, 3])(update) + def test_filters_dice_type(self, update): + update.message.dice = Dice(5, '🎲') + assert Filters.dice.dice(update) + assert Filters.dice.dice([4, 5])(update) + assert not Filters.dice.darts(update) + assert not Filters.dice.dice([6])(update) + + update.message.dice = Dice(5, '🎯') + assert Filters.dice.darts(update) + assert Filters.dice.darts([4, 5])(update) + assert not Filters.dice.dice(update) + assert not Filters.dice.darts([6])(update) + def test_language_filter_single(self, update): update.message.from_user.language_code = 'en_US' assert (Filters.language('en_US'))(update) diff --git a/tests/test_message.py b/tests/test_message.py index c37f2ad810b..8114684df88 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -98,7 +98,7 @@ def message(bot): 'text': 'next', 'callback_data': 'abcd'}], [{'text': 'Cancel', 'callback_data': 'Cancel'}]]}}, {'quote': True}, - {'dice': Dice(4)} + {'dice': Dice(4, '🎲')} ], ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text', 'caption_entities', 'audio', 'document', 'animation', 'game', 'photo', diff --git a/tests/test_poll.py b/tests/test_poll.py index 00dc7940c99..bc1c0542064 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -19,7 +19,9 @@ import pytest -from telegram import Poll, PollOption, PollAnswer, User +from datetime import datetime +from telegram import Poll, PollOption, PollAnswer, User, MessageEntity +from telegram.utils.helpers import to_timestamp @pytest.fixture(scope="class") @@ -91,7 +93,11 @@ def poll(): TestPoll.is_closed, TestPoll.is_anonymous, TestPoll.type, - TestPoll.allows_multiple_answers + TestPoll.allows_multiple_answers, + explanation=TestPoll.explanation, + explanation_entities=TestPoll.explanation_entities, + open_period=TestPoll.open_period, + close_date=TestPoll.close_date, ) @@ -104,6 +110,11 @@ class TestPoll(object): is_anonymous = False type = Poll.REGULAR allows_multiple_answers = True + explanation = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467' + b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape') + explanation_entities = [MessageEntity(13, 17, MessageEntity.URL)] + open_period = 42 + close_date = datetime.utcnow() def test_de_json(self): json_dict = { @@ -114,7 +125,11 @@ def test_de_json(self): 'is_closed': self.is_closed, 'is_anonymous': self.is_anonymous, 'type': self.type, - 'allows_multiple_answers': self.allows_multiple_answers + 'allows_multiple_answers': self.allows_multiple_answers, + 'explanation': self.explanation, + 'explanation_entities': [self.explanation_entities[0].to_dict()], + 'open_period': self.open_period, + 'close_date': to_timestamp(self.close_date) } poll = Poll.de_json(json_dict, None) @@ -130,6 +145,11 @@ def test_de_json(self): assert poll.is_anonymous == self.is_anonymous assert poll.type == self.type assert poll.allows_multiple_answers == self.allows_multiple_answers + assert poll.explanation == self.explanation + assert poll.explanation_entities == self.explanation_entities + assert poll.open_period == self.open_period + assert pytest.approx(poll.close_date == self.close_date) + assert to_timestamp(poll.close_date) == to_timestamp(self.close_date) def test_to_dict(self, poll): poll_dict = poll.to_dict() @@ -143,3 +163,21 @@ def test_to_dict(self, poll): assert poll_dict['is_anonymous'] == poll.is_anonymous assert poll_dict['type'] == poll.type assert poll_dict['allows_multiple_answers'] == poll.allows_multiple_answers + assert poll_dict['explanation'] == poll.explanation + assert poll_dict['explanation_entities'] == [poll.explanation_entities[0].to_dict()] + assert poll_dict['open_period'] == poll.open_period + assert poll_dict['close_date'] == to_timestamp(poll.close_date) + + def test_parse_entity(self, poll): + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + poll.explanation_entities = [entity] + + assert poll.parse_explanation_entity(entity) == 'http://google.com' + + def test_parse_entities(self, poll): + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1) + poll.explanation_entities = [entity_2, entity] + + assert poll.parse_explanation_entities(MessageEntity.URL) == {entity: 'http://google.com'} + assert poll.parse_explanation_entities() == {entity: 'http://google.com', entity_2: 'h'} From 9a5217d8e1ed67d01da73328ebae96f16aee33f9 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 24 Apr 2020 16:42:03 +0200 Subject: [PATCH 2/6] Elaborate docs --- telegram/dice.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/telegram/dice.py b/telegram/dice.py index e803fbfa9bd..192a7d1e86b 100644 --- a/telegram/dice.py +++ b/telegram/dice.py @@ -27,6 +27,11 @@ class Dice(TelegramObject): (The singular form of "dice" is "die". However, PTB mimics the Telegram API, which uses the term "dice".) + Note: + If :attr:`emoji` is "🎯", a value of 6 represents a bullseye, while a value of 1 indicates + that the dartboard was missed. However, this behaviour is undocumented and might be changed + by Telegram. + Attributes: value (:obj:`int`): Value of the dice. emoji (:obj:`str`): Emoji on which the dice throw animation is based. From ecf6ab5fa7188703ce5241e5bbe4626f43d4fd6a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 1 May 2020 09:04:34 +0200 Subject: [PATCH 3/6] Address review --- telegram/dice.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/telegram/dice.py b/telegram/dice.py index 192a7d1e86b..d1cbff6b3aa 100644 --- a/telegram/dice.py +++ b/telegram/dice.py @@ -28,9 +28,9 @@ class Dice(TelegramObject): term "dice".) Note: - If :attr:`emoji` is "🎯", a value of 6 represents a bullseye, while a value of 1 indicates - that the dartboard was missed. However, this behaviour is undocumented and might be changed - by Telegram. + If :attr:`emoji` is "🎯", a value of 6 currently represents a bullseye, while a value of 1 + indicates that the dartboard was missed. However, this behaviour is undocumented and might + be changed by Telegram. Attributes: value (:obj:`int`): Value of the dice. @@ -51,5 +51,10 @@ def de_json(cls, data, bot): return cls(**data) - ALL_EMOJI = ['🎲', '🎯'] - """List[:obj:`str`]: List of all supported base emoji.""" + DICE = '🎲' + """:obj:`str`: '🎲'""" + DARTS = '🎯' + """:obj:`str`: '🎯'""" + ALL_EMOJI = [DICE, DARTS] + """List[:obj:`str`]: List of all supported base emoji. Currently :attr:`DICE` and + :attr:`DARTS`.""" From cc684ddffb72e1df87c730070bcb33eecf29ab3b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 1 May 2020 14:29:56 +0200 Subject: [PATCH 4/6] Fix Message.to_json/dict() test --- tests/test_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_message.py b/tests/test_message.py index 8114684df88..ef270431fbd 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -92,7 +92,7 @@ def message(bot): options=[PollOption(text='a', voter_count=1), PollOption(text='b', voter_count=2)], is_closed=False, total_voter_count=0, is_anonymous=False, type=Poll.REGULAR, - allows_multiple_answers=True)}, + allows_multiple_answers=True, explanation_entities=[])}, {'text': 'a text message', 'reply_markup': {'inline_keyboard': [[{ 'text': 'start', 'url': 'http://google.com'}, { 'text': 'next', 'callback_data': 'abcd'}], From be7bc3151fffaed2b9f4968f802cc2fcfda80523 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 1 May 2020 14:56:57 +0200 Subject: [PATCH 5/6] More coverage --- telegram/ext/filters.py | 9 +++------ tests/test_bot.py | 7 +++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 0f96fa6adec..2246a9903e9 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -223,13 +223,10 @@ def __init__(self, emoji=None, name=None): class _DiceValues(BaseFilter): - def __init__(self, values, emoji=None, name=None): + def __init__(self, values, name, emoji=None): self.values = [values] if isinstance(values, int) else values self.emoji = emoji - if name: - self.name = 'Filters.dice.{}({})'.format(name, values) - else: - self.name = 'Filters.dice({})'.format(values) + self.name = '{}({})'.format(name, values) def filter(self, message): if bool(message.dice and message.dice.value in self.values): @@ -241,7 +238,7 @@ def __call__(self, update): if isinstance(update, Update): return self.filter(update.effective_message) else: - return self._DiceValues(update, emoji=self.emoji, name=self.name) + return self._DiceValues(update, self.name, emoji=self.emoji) def filter(self, message): if bool(message.dice): diff --git a/tests/test_bot.py b/tests/test_bot.py index 6abd22619d0..e7cdab186e8 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -285,12 +285,15 @@ def test_send_poll_default_parse_mode(self, default_bot, super_group_id): @flaky(3, 1) @pytest.mark.timeout(10) - @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI) + @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI + [None]) def test_send_dice(self, bot, chat_id, emoji): message = bot.send_dice(chat_id, emoji=emoji) assert message.dice - assert message.dice.emoji == emoji + if emoji is None: + assert message.dice.emoji == Dice.DICE + else: + assert message.dice.emoji == emoji @flaky(3, 1) @pytest.mark.timeout(10) From 427a5d9fd016cc9ccb432f2caa015f16cb40c22e Mon Sep 17 00:00:00 2001 From: Bibo-Joshi Date: Fri, 1 May 2020 22:39:50 +0200 Subject: [PATCH 6/6] Update telegram/bot.py Co-authored-by: Noam Meltzer --- telegram/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/bot.py b/telegram/bot.py index 029988cdc86..1ff127cc5ff 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -3790,7 +3790,7 @@ def send_dice(self, Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat. emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based. - Currently, must be one of “🎲” or “🎯”. Defauts to “🎲” + Currently, must be one of “🎲” or “🎯”. Defaults to “🎲” 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