diff --git a/telegram/bot.py b/telegram/bot.py index 362f2daca..c0ab44ca7 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -91,6 +91,8 @@ class Bot(TelegramObject): `disable_web_page_preview` parameter used if not set explicitly in method call. default_timeout (:obj:`int` | :obj:`float`, optional): Default setting for the `timeout` parameter used if not set explicitly in method call. + default_quote (:obj:`bool`, optional): Default setting for the `quote` parameter of the + :attr:`telegram.Message.reply_text` and friends. """ @@ -136,6 +138,7 @@ class Bot(TelegramObject): default_parse_mode=None, default_disable_notification=None, default_disable_web_page_preview=None, + default_quote=None, # Timeout needs special treatment, since the bot methods have two different # default values for timeout (None and 20s) default_timeout=DEFAULT_NONE): @@ -145,7 +148,8 @@ class Bot(TelegramObject): self.defaults = Defaults(parse_mode=default_parse_mode, disable_notification=default_disable_notification, disable_web_page_preview=default_disable_web_page_preview, - timeout=default_timeout) + timeout=default_timeout, + quote=default_quote) if base_url is None: base_url = 'https://api.telegram.org/bot' @@ -186,6 +190,8 @@ class Bot(TelegramObject): if result is True: return result + result['default_quote'] = self.defaults.quote + return Message.de_json(result, self) @property @@ -1050,6 +1056,9 @@ class Bot(TelegramObject): result = self._request.post(url, data, timeout=timeout) + for res in result: + res['default_quote'] = self.defaults.quote + return [Message.de_json(res, self) for res in result] @log @@ -2063,6 +2072,9 @@ class Bot(TelegramObject): else: self.logger.debug('No new updates found.') + for u in result: + u['default_quote'] = self.defaults.quote + return [Update.de_json(u, self) for u in result] @log @@ -2238,6 +2250,8 @@ class Bot(TelegramObject): result = self._request.post(url, data, timeout=timeout) + result['default_quote'] = self.defaults.quote + return Chat.de_json(result, self) @log diff --git a/telegram/callbackquery.py b/telegram/callbackquery.py index ac9e6570b..018f29a9c 100644 --- a/telegram/callbackquery.py +++ b/telegram/callbackquery.py @@ -101,7 +101,10 @@ class CallbackQuery(TelegramObject): data = super(CallbackQuery, cls).de_json(data, bot) data['from_user'] = User.de_json(data.get('from'), bot) - data['message'] = Message.de_json(data.get('message'), bot) + message = data.get('message') + if message: + message['default_quote'] = data.get('default_quote') + data['message'] = Message.de_json(message, bot) return cls(bot=bot, **data) diff --git a/telegram/chat.py b/telegram/chat.py index 2afc03ee6..077e669d9 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -135,7 +135,10 @@ class Chat(TelegramObject): data['photo'] = ChatPhoto.de_json(data.get('photo'), bot) from telegram import Message - data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot) + pinned_message = data.get('pinned_message') + if pinned_message: + pinned_message['default_quote'] = data.get('default_quote') + data['pinned_message'] = Message.de_json(pinned_message, bot) data['permissions'] = ChatPermissions.de_json(data.get('permissions'), bot) return cls(bot=bot, **data) diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 46519c8d6..dc4c5d72e 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -89,6 +89,8 @@ class Updater(object): `disable_web_page_preview` parameter used if not set explicitly in method call. default_timeout (:obj:`int` | :obj:`float`, optional): Default setting for the `timeout` parameter used if not set explicitly in method call. + default_quote (:obj:`bool`, optional): Default setting for the `quote` parameter of the + :attr:`telegram.Message.reply_text` and friends. Note: You must supply either a :attr:`bot` or a :attr:`token` argument. @@ -114,6 +116,7 @@ class Updater(object): default_disable_notification=None, default_disable_web_page_preview=None, default_timeout=DEFAULT_NONE, + default_quote=None, use_context=False): if (token is None) and (bot is None): @@ -150,7 +153,8 @@ class Updater(object): default_parse_mode=default_parse_mode, default_disable_notification=default_disable_notification, default_disable_web_page_preview=default_disable_web_page_preview, - default_timeout=default_timeout) + default_timeout=default_timeout, + default_quote=default_quote) self.user_sig_handler = user_sig_handler self.update_queue = Queue() self.job_queue = JobQueue() @@ -172,6 +176,9 @@ class Updater(object): self.__lock = Lock() self.__threads = [] + # Just for passing to WebhookAppClass + self._default_quote = default_quote + def _init_thread(self, target, name, *args, **kwargs): thr = Thread(target=self._thread_wrapper, name="Bot:{}:{}".format(self.bot.id, name), args=(target,) + args, kwargs=kwargs) @@ -386,7 +393,8 @@ class Updater(object): url_path = '/{0}'.format(url_path) # Create Tornado app instance - app = WebhookAppClass(url_path, self.bot, self.update_queue) + app = WebhookAppClass(url_path, self.bot, self.update_queue, + default_quote=self._default_quote) # Form SSL Context # An SSLError is raised if the private key does not match with the certificate diff --git a/telegram/message.py b/telegram/message.py index 0879427d7..abe7174b4 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -109,6 +109,8 @@ class Message(TelegramObject): reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + default_quote (:obj:`bool`): Optional. Default setting for the `quote` parameter of the + :attr:`reply_text` and friends. Args: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -214,6 +216,8 @@ class Message(TelegramObject): information about the poll. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. + default_quote (:obj:`bool`, optional): Default setting for the `quote` parameter of the + :attr:`reply_text` and friends. """ @@ -277,6 +281,7 @@ class Message(TelegramObject): forward_sender_name=None, reply_markup=None, bot=None, + default_quote=None, **kwargs): # Required self.message_id = int(message_id) @@ -328,6 +333,7 @@ class Message(TelegramObject): self.poll = poll self.reply_markup = reply_markup self.bot = bot + self.default_quote = default_quote self._id_attrs = (self.message_id,) @@ -363,13 +369,22 @@ class Message(TelegramObject): data['from_user'] = User.de_json(data.get('from'), bot) data['date'] = from_timestamp(data['date']) - data['chat'] = Chat.de_json(data.get('chat'), bot) + chat = data.get('chat') + if chat: + chat['default_quote'] = data.get('default_quote') + data['chat'] = Chat.de_json(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) + forward_from_chat = data.get('forward_from_chat') + if forward_from_chat: + forward_from_chat['default_quote'] = data.get('default_quote') + data['forward_from_chat'] = Chat.de_json(forward_from_chat, bot) data['forward_date'] = from_timestamp(data.get('forward_date')) - data['reply_to_message'] = Message.de_json(data.get('reply_to_message'), bot) + reply_to_message = data.get('reply_to_message') + if reply_to_message: + reply_to_message['default_quote'] = data.get('default_quote') + data['reply_to_message'] = Message.de_json(reply_to_message, bot) data['edit_date'] = from_timestamp(data.get('edit_date')) data['audio'] = Audio.de_json(data.get('audio'), bot) data['document'] = Document.de_json(data.get('document'), bot) @@ -386,7 +401,10 @@ class Message(TelegramObject): data['new_chat_members'] = User.de_list(data.get('new_chat_members'), bot) data['left_chat_member'] = User.de_json(data.get('left_chat_member'), bot) data['new_chat_photo'] = PhotoSize.de_list(data.get('new_chat_photo'), bot) - data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot) + pinned_message = data.get('pinned_message') + if pinned_message: + pinned_message['default_quote'] = data.get('default_quote') + data['pinned_message'] = Message.de_json(pinned_message, bot) data['invoice'] = Invoice.de_json(data.get('invoice'), bot) data['successful_payment'] = SuccessfulPayment.de_json(data.get('successful_payment'), bot) data['passport_data'] = PassportData.de_json(data.get('passport_data'), bot) @@ -469,7 +487,8 @@ class Message(TelegramObject): del kwargs['quote'] else: - if self.chat.type != Chat.PRIVATE: + if ((self.default_quote is None and self.chat.type != Chat.PRIVATE) + or self.default_quote): kwargs['reply_to_message_id'] = self.message_id def reply_text(self, *args, **kwargs): diff --git a/telegram/update.py b/telegram/update.py index b05d66124..a6b16ef36 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -211,16 +211,31 @@ class Update(TelegramObject): data = super(Update, cls).de_json(data, bot) - data['message'] = Message.de_json(data.get('message'), bot) - data['edited_message'] = Message.de_json(data.get('edited_message'), bot) + message = data.get('message') + if message: + message['default_quote'] = data.get('default_quote') + data['message'] = Message.de_json(message, bot) + edited_message = data.get('edited_message') + if edited_message: + edited_message['default_quote'] = data.get('default_quote') + data['edited_message'] = Message.de_json(edited_message, bot) data['inline_query'] = InlineQuery.de_json(data.get('inline_query'), bot) data['chosen_inline_result'] = ChosenInlineResult.de_json( data.get('chosen_inline_result'), bot) - data['callback_query'] = CallbackQuery.de_json(data.get('callback_query'), bot) + callback_query = data.get('callback_query') + if callback_query: + callback_query['default_quote'] = data.get('default_quote') + data['callback_query'] = CallbackQuery.de_json(callback_query, bot) data['shipping_query'] = ShippingQuery.de_json(data.get('shipping_query'), bot) data['pre_checkout_query'] = PreCheckoutQuery.de_json(data.get('pre_checkout_query'), bot) - data['channel_post'] = Message.de_json(data.get('channel_post'), bot) - data['edited_channel_post'] = Message.de_json(data.get('edited_channel_post'), bot) + channel_post = data.get('channel_post') + if channel_post: + channel_post['default_quote'] = data.get('default_quote') + data['channel_post'] = Message.de_json(channel_post, bot) + edited_channel_post = data.get('edited_channel_post') + if edited_channel_post: + edited_channel_post['default_quote'] = data.get('default_quote') + data['edited_channel_post'] = Message.de_json(edited_channel_post, bot) data['poll'] = Poll.de_json(data.get('poll'), bot) return cls(**data) diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index 6d245c9c7..c7a49b258 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -432,6 +432,9 @@ class Defaults: 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). + quote (:obj:`bool`): Optional. If set to ``True``, the reply is sent as an actual reply to + the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will + be ignored. Default: ``True`` in group chats and ``False`` in private chats. Parameters: parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show @@ -443,6 +446,9 @@ class Defaults: 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). + quote (:obj:`bool`, opitonal): If set to ``True``, the reply is sent as an actual reply to + the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will + be ignored. Default: ``True`` in group chats and ``False`` in private chats. """ def __init__(self, parse_mode=None, @@ -450,17 +456,20 @@ class Defaults: disable_web_page_preview=None, # Timeout needs special treatment, since the bot methods have two different # default values for timeout (None and 20s) - timeout=DEFAULT_NONE): + timeout=DEFAULT_NONE, + quote=None): self.parse_mode = parse_mode self.disable_notification = disable_notification self.disable_web_page_preview = disable_web_page_preview self.timeout = timeout + self.quote = quote def __hash__(self): return hash((self.parse_mode, self.disable_notification, self.disable_web_page_preview, - self.timeout)) + self.timeout, + self.quote)) def __eq__(self, other): if isinstance(other, Defaults): diff --git a/telegram/utils/webhookhandler.py b/telegram/utils/webhookhandler.py index f570a4352..c76a624db 100644 --- a/telegram/utils/webhookhandler.py +++ b/telegram/utils/webhookhandler.py @@ -71,8 +71,9 @@ class WebhookServer(object): class WebhookAppClass(tornado.web.Application): - def __init__(self, webhook_path, bot, update_queue): - self.shared_objects = {"bot": bot, "update_queue": update_queue} + def __init__(self, webhook_path, bot, update_queue, default_quote=None): + self.shared_objects = {"bot": bot, "update_queue": update_queue, + "default_quote": default_quote} handlers = [ (r"{0}/?".format(webhook_path), WebhookHandler, self.shared_objects) @@ -91,9 +92,10 @@ class WebhookHandler(tornado.web.RequestHandler): super(WebhookHandler, self).__init__(application, request, **kwargs) self.logger = logging.getLogger(__name__) - def initialize(self, bot, update_queue): + def initialize(self, bot, update_queue, default_quote=None): self.bot = bot self.update_queue = update_queue + self._default_quote = default_quote def set_default_headers(self): self.set_header("Content-Type", 'application/json; charset="utf-8"') @@ -105,6 +107,7 @@ class WebhookHandler(tornado.web.RequestHandler): data = json.loads(json_string) self.set_status(200) self.logger.debug('Webhook received data: ' + json_string) + data['default_quote'] = self._default_quote update = Update.de_json(data, self.bot) self.logger.debug('Received Update with ID %d on Webhook' % update.update_id) self.update_queue.put(update) diff --git a/tests/test_bot.py b/tests/test_bot.py index dee789150..347905250 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -467,6 +467,21 @@ class TestBot(object): assert chat.title == '>>> telegram.Bot(test)' assert chat.id == int(super_group_id) + # TODO: Add bot to group to test there too + @flaky(3, 1) + @pytest.mark.timeout(10) + @pytest.mark.parametrize('default_bot', [{'default_quote': True}], indirect=True) + def test_get_chat_default_quote(self, default_bot, super_group_id): + message = default_bot.send_message(super_group_id, text="test_get_chat_default_quote") + default_bot.pin_chat_message(chat_id=super_group_id, message_id=message.message_id, + disable_notification=True) + + chat = default_bot.get_chat(super_group_id) + chat.pinned_message == message + assert chat.pinned_message.default_quote is True + + default_bot.unpinChatMessage(super_group_id) + @flaky(3, 1) @pytest.mark.timeout(10) def test_get_chat_administrators(self, bot, channel_id): @@ -799,3 +814,10 @@ class TestBot(object): message = default_bot.send_message(chat_id, test_markdown_string, parse_mode='HTML') assert message.text == test_markdown_string assert message.text_markdown == escape_markdown(test_markdown_string) + + @flaky(3, 1) + @pytest.mark.timeout(10) + @pytest.mark.parametrize('default_bot', [{'default_quote': True}], indirect=True) + def test_send_message_default_quote(self, default_bot, chat_id): + message = default_bot.send_message(chat_id, 'test') + assert message.default_quote is True diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 870d64453..866a39593 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -53,13 +53,15 @@ class TestCallbackQuery(object): 'message': self.message.to_dict(), 'data': self.data, 'inline_message_id': self.inline_message_id, - 'game_short_name': self.game_short_name} + 'game_short_name': self.game_short_name, + 'default_quote': True} callback_query = CallbackQuery.de_json(json_dict, bot) assert callback_query.id == self.id assert callback_query.from_user == self.from_user assert callback_query.chat_instance == self.chat_instance assert callback_query.message == self.message + assert callback_query.message.default_quote is True assert callback_query.data == self.data assert callback_query.inline_message_id == self.inline_message_id assert callback_query.game_short_name == self.game_short_name diff --git a/tests/test_chat.py b/tests/test_chat.py index 3b39dcd46..d877cee8a 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -20,7 +20,7 @@ import pytest from telegram import Chat, ChatAction, ChatPermissions -from telegram import User +from telegram import User, Message @pytest.fixture(scope='class') @@ -68,6 +68,22 @@ class TestChat(object): assert chat.can_set_sticker_set == self.can_set_sticker_set assert chat.permissions == self.permissions + def test_de_json_default_quote(self, bot): + json_dict = { + 'id': self.id, + 'type': self.type, + 'pinned_message': Message( + message_id=123, + from_user=None, + date=None, + chat=None + ).to_dict(), + 'default_quote': True + } + chat = Chat.de_json(json_dict, bot) + + assert chat.pinned_message.default_quote is True + def test_to_dict(self, chat): chat_dict = chat.to_dict() diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index 4cad93fff..71481a89f 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -329,6 +329,13 @@ class TestSendMediaGroup(object): assert all([isinstance(mes, Message) for mes in messages]) assert all([mes.media_group_id == messages[0].media_group_id for mes in messages]) + @flaky(3, 1) + @pytest.mark.timeout(10) + @pytest.mark.parametrize('default_bot', [{'default_quote': True}], indirect=True) + def test_send_media_group_default_quote(self, default_bot, chat_id, media_group): + messages = default_bot.send_media_group(chat_id, media_group) + assert all([mes.default_quote is True for mes in messages]) + @flaky(3, 1) @pytest.mark.timeout(10) def test_edit_message_media(self, bot, chat_id, media_group): diff --git a/tests/test_message.py b/tests/test_message.py index a27612fdb..5a8c22313 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -94,7 +94,8 @@ def message(bot): {'text': 'a text message', 'reply_markup': {'inline_keyboard': [[{ 'text': 'start', 'url': 'http://google.com'}, { 'text': 'next', 'callback_data': 'abcd'}], - [{'text': 'Cancel', 'callback_data': 'Cancel'}]]}} + [{'text': 'Cancel', 'callback_data': 'Cancel'}]]}}, + {'default_quote': True} ], ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text', 'caption_entities', 'audio', 'document', 'animation', 'game', 'photo', @@ -103,7 +104,8 @@ def message(bot): 'group_created', 'supergroup_created', 'channel_created', 'migrated_to', 'migrated_from', 'pinned', 'invoice', 'successful_payment', 'connected_website', 'forward_signature', 'author_signature', - 'photo_from_media_group', 'passport_data', 'poll', 'reply_markup']) + 'photo_from_media_group', 'passport_data', 'poll', 'reply_markup', + 'default_quote']) def message_params(bot, request): return Message(message_id=TestMessage.id, from_user=TestMessage.from_user, @@ -653,6 +655,27 @@ class TestMessage(object): monkeypatch.setattr(message.bot, 'delete_message', test) assert message.delete() + def test_default_quote(self, message): + kwargs = {} + + message.default_quote = False + message._quote(kwargs) + assert 'reply_to_message_id' not in kwargs + + message.default_quote = True + message._quote(kwargs) + assert 'reply_to_message_id' in kwargs + + kwargs = {} + message.default_quote = None + message.chat.type = Chat.PRIVATE + message._quote(kwargs) + assert 'reply_to_message_id' not in kwargs + + message.chat.type = Chat.GROUP + message._quote(kwargs) + assert 'reply_to_message_id' in kwargs + def test_equality(self): id = 1 a = Message(id, self.from_user, self.date, self.chat) diff --git a/tests/test_update.py b/tests/test_update.py index 59702207a..b87d1f750 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -75,6 +75,14 @@ class TestUpdate(object): assert update is None + def test_de_json_default_quote(self, bot): + json_dict = {'update_id': TestUpdate.update_id} + json_dict['message'] = message.to_dict() + json_dict['default_quote'] = True + update = Update.de_json(json_dict, bot) + + assert update.message.default_quote is True + def test_to_dict(self, update): update_dict = update.to_dict() diff --git a/tests/test_updater.py b/tests/test_updater.py index a23d15c06..3606f242a 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -221,6 +221,30 @@ class TestUpdater(object): assert q.get(False) == update updater.stop() + def test_webhook_default_quote(self, monkeypatch, updater): + updater._default_quote = True + q = Queue() + monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) + monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) + monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u)) + + ip = '127.0.0.1' + port = randrange(1024, 49152) # Select random port for travis + updater.start_webhook( + ip, + port, + url_path='TOKEN') + sleep(.2) + + # Now, we send an update to the server via urlopen + update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''), + text='Webhook')) + self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN') + sleep(.2) + # assert q.get(False) == update + assert q.get(False).message.default_quote is True + updater.stop() + @pytest.mark.parametrize(('error',), argvalues=[(TelegramError(''),)], ids=('TelegramError',))