From 9af904ec2c06349af135202b5c8cf9db3dc7b68d Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Sun, 8 Nov 2020 12:30:20 +0100 Subject: [PATCH 1/6] adding classes and methods --- docs/source/telegram.messageid.rst | 6 ++ docs/source/telegram.rst | 1 + telegram/__init__.py | 2 + telegram/bot.py | 89 +++++++++++++++++++++++++++++- telegram/constants.py | 3 +- telegram/message.py | 19 ++++++- telegram/messageid.py | 39 +++++++++++++ telegram/poll.py | 6 +- tests/test_bot.py | 26 +++++++++ tests/test_message.py | 24 ++++++++ 10 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 docs/source/telegram.messageid.rst create mode 100644 telegram/messageid.py diff --git a/docs/source/telegram.messageid.rst b/docs/source/telegram.messageid.rst new file mode 100644 index 00000000000..9df1de2a145 --- /dev/null +++ b/docs/source/telegram.messageid.rst @@ -0,0 +1,6 @@ +telegram.MessageId +================== + +.. autoclass:: telegram.MessageId + :members: + :show-inheritance: diff --git a/docs/source/telegram.rst b/docs/source/telegram.rst index ddb87d773af..a438e0a1f85 100644 --- a/docs/source/telegram.rst +++ b/docs/source/telegram.rst @@ -37,6 +37,7 @@ telegram package telegram.location telegram.loginurl telegram.message + telegram.messageid telegram.messageentity telegram.parsemode telegram.photosize diff --git a/telegram/__init__.py b/telegram/__init__.py index a9f8d4e9b91..0a455ef5301 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -50,6 +50,7 @@ from .files.file import File from .parsemode import ParseMode from .messageentity import MessageEntity +from .messageid import MessageId from .games.game import Game from .poll import Poll, PollOption, PollAnswer from .loginurl import LoginUrl @@ -268,4 +269,5 @@ 'KeyboardButtonPollType', 'Dice', 'BotCommand', + 'MessageId', ] diff --git a/telegram/bot.py b/telegram/bot.py index e90993fcd54..c9e7694b240 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -69,6 +69,8 @@ Location, MaskPosition, Message, + MessageEntity, + MessageId, PassportElementError, PhotoSize, Poll, @@ -94,7 +96,7 @@ from telegram.error import InvalidToken, TelegramError from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, to_timestamp from telegram.utils.request import Request -from telegram.utils.types import FileLike, JSONDict +from telegram.utils.types import FileLike, JSONDict, SLT if TYPE_CHECKING: from telegram.ext import Defaults @@ -4371,6 +4373,89 @@ def set_my_commands( return result # type: ignore[return-value] + @log + def copy_message( + self, + chat_id: Union[int, str], + from_chat_id: Union[str, int], + message_id: Union[str, int], + caption: str = None, + parse_mode: str = None, + caption_entities: SLT[MessageEntity] = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + allow_sending_without_reply: bool = False, + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None, + ) -> Optional[MessageId]: + """ + Use this method to copy messages of any kind. The method is analogous to the method + forwardMessages, but the copied message doesn't have a link to the original message. + Returns the MessageId of the sent message on success. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format @channelusername). + from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the + original message was sent (or channel username in the format @channelusername). + message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id. + caption (:obj:`str`, optional): New caption for media, 0-1024 characters after + entities parsing. If not specified, the original caption is kept. + parse_mode (:obj:`str`, optional): Mode for parsing entities in the new caption. See + the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (:class:`telegram.utils.types.SLT[MessageEntity]`): List of special + entities that appear in the new caption, which can be specified instead of + parse_mode + 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 + original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. + 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). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :class:`telegram.MessageId`: On success + + Raises: + :class:`telegram.TelegramError` + """ + data: JSONDict = { + 'chat_id': chat_id, + 'from_chat_id': from_chat_id, + 'message_id': message_id, + } + if caption: + data['caption'] = caption + if parse_mode: + data['parse_mode'] = parse_mode + if caption_entities: + data['caption_entities'] = caption_entities + if disable_notification: + data['disable_notification'] = disable_notification + if reply_to_message_id: + data['reply_to_message_id'] = reply_to_message_id + if allow_sending_without_reply: + data['allow_sending_without_reply'] = allow_sending_without_reply + if reply_markup: + if isinstance(reply_markup, ReplyMarkup): + # We need to_json() instead of to_dict() here, because reply_markups may be + # attached to media messages, which aren't json dumped by utils.request + data['reply_markup'] = reply_markup.to_json() + else: + data['reply_markup'] = reply_markup + + result = self._post('copyMessage', data, timeout=timeout, api_kwargs=api_kwargs) + return MessageId.de_json(result, self) # type: ignore + def to_dict(self) -> JSONDict: data: JSONDict = {'id': self.id, 'username': self.username, 'first_name': self.first_name} @@ -4520,3 +4605,5 @@ def to_dict(self) -> JSONDict: """Alias for :attr:`get_my_commands`""" setMyCommands = set_my_commands """Alias for :attr:`set_my_commands`""" + copyMessage = copy_message + """Alias for :attr:`copy_message`""" diff --git a/telegram/constants.py b/telegram/constants.py index 188c9c24308..65760a014fe 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -119,6 +119,7 @@ Attributes: POLL_REGULAR (:obj:`str`): 'regular' POLL_QUIZ (:obj:`str`): 'quiz' + POLL_QUESTION_LENGTH (:obj:`int`): 300 :class:`telegram.files.MaskPosition`: @@ -223,7 +224,7 @@ POLL_REGULAR: str = 'regular' POLL_QUIZ: str = 'quiz' - +POLL_QUESTION_LENGTH: int = 300 STICKER_FOREHEAD: str = 'forehead' STICKER_EYES: str = 'eyes' diff --git a/telegram/message.py b/telegram/message.py index 7f2d2ab9eee..38fbcf9cab0 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -52,7 +52,7 @@ from telegram.utils.types import JSONDict if TYPE_CHECKING: - from telegram import Bot, GameHighScore, InputMedia + from telegram import Bot, GameHighScore, InputMedia, MessageId _UNDEFINED = object() @@ -924,6 +924,23 @@ def forward(self, chat_id: int, *args: Any, **kwargs: Any) -> 'Message': chat_id=chat_id, from_chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs ) + def copy(self, chat_id: int, *args: Any, **kwargs: Any) -> 'MessageId': + """Shortcut for:: + + bot.copy_message(chat_id=chat_id, + from_chat_id=update.message.chat_id, + message_id=update.message.message_id, + *args, + **kwargs) + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + return self.bot.copy_message( + chat_id=chat_id, from_chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs + ) + def edit_text(self, *args: Any, **kwargs: Any) -> Union['Message', bool]: """Shortcut for:: diff --git a/telegram/messageid.py b/telegram/messageid.py new file mode 100644 index 00000000000..bdc44e8d755 --- /dev/null +++ b/telegram/messageid.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2020 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a type of a Telegram Poll.""" +from typing import Any + +from telegram import TelegramObject + + +class MessageId(TelegramObject): + """This object represents a unique message identifier. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_id` is equal. + + Attributes: + message_id (:obj:`int`): Unique message identifier + """ + + def __init__(self, message_id: int, **_kwargs: Any): # pylint: disable=W0622 + self.message_id = message_id + + self._id_attrs = (self.message_id,) diff --git a/telegram/poll.py b/telegram/poll.py index 5c9682cbde7..42c2080fe19 100644 --- a/telegram/poll.py +++ b/telegram/poll.py @@ -39,11 +39,11 @@ class PollOption(TelegramObject): considered equal, if their :attr:`text` and :attr:`voter_count` are equal. Attributes: - text (:obj:`str`): Option text, 1-100 characters. + text (:obj:`str`): Option text, 1-300 characters. voter_count (:obj:`int`): Number of users that voted for this option. Args: - text (:obj:`str`): Option text, 1-100 characters. + text (:obj:`str`): Option text, 1-300 characters. voter_count (:obj:`int`): Number of users that voted for this option. """ @@ -262,3 +262,5 @@ def parse_explanation_entities(self, types: List[str] = None) -> Dict[MessageEnt """:const:`telegram.constants.POLL_REGULAR`""" QUIZ: ClassVar[str] = constants.POLL_QUIZ """:const:`telegram.constants.POLL_QUIZ`""" + POLL_QUESTION_LENGTH: ClassVar[int] = constants.POLL_QUESTION_LENGTH + """:const:`telegram.constants.POLL_QUESTION_LENGTH`""" diff --git a/tests/test_bot.py b/tests/test_bot.py index f12e51671fe..21d52de2bf5 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1379,3 +1379,29 @@ def test_set_and_get_my_commands_strings(self, bot): assert bc[0].description == 'descr1' assert bc[1].command == 'cmd2' assert bc[1].description == 'descr2' + + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_copy_message(self, monkeypatch, bot, chat_id, media_message): + keyboard = [[InlineKeyboardButton(text="test", callback_data="test2")]] + + def post(url, data, timeout): + assert data["chat_id"] == chat_id + assert data["from_chat_id"] == chat_id + assert data["message_id"] == media_message.message_id + assert data["caption"] == "Test" + assert data["parse_mode"] == ParseMode.HTML + assert data["reply_to_message_id"] == media_message.message_id + assert data["reply_markup"] == keyboard + return data + + monkeypatch.setattr(bot.request, 'post', post) + bot.copy_message( + chat_id, + from_chat_id=chat_id, + message_id=media_message.message_id, + caption="Test", + parse_mode=ParseMode.HTML, + reply_to_message_id=media_message.message_id, + reply_markup=keyboard, + ) diff --git a/tests/test_message.py b/tests/test_message.py index ab25f4da5f5..da3a7582251 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -944,6 +944,30 @@ def test(*args, **kwargs): assert message.forward(123456, disable_notification=True) assert not message.forward(635241) + @pytest.mark.test + def test_copy(self, monkeypatch, message): + keyboard = [[1, 2]] + + def test(*args, **kwargs): + chat_id = kwargs['chat_id'] == 123456 + from_chat = kwargs['from_chat_id'] == message.chat_id + message_id = kwargs['message_id'] == message.message_id + if kwargs.get('disable_notification'): + notification = kwargs['disable_notification'] is True + else: + notification = True + if kwargs.get('reply_markup'): + reply_markup = kwargs['reply_markup'] is keyboard + else: + reply_markup = True + return chat_id and from_chat and message_id and notification and reply_markup + + monkeypatch.setattr(message.bot, 'copy_message', test) + assert message.copy(123456) + assert message.copy(123456, disable_notification=True) + assert message.copy(123456, reply_markup=keyboard) + assert not message.copy(635241) + def test_edit_text(self, monkeypatch, message): def test(*args, **kwargs): chat_id = kwargs['chat_id'] == message.chat_id From efd9d4841fdd1eca9a3e08fe7236a2d71c519396 Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Tue, 17 Nov 2020 22:10:38 +0100 Subject: [PATCH 2/6] adressing review comments --- telegram/bot.py | 4 +-- telegram/callbackquery.py | 18 +++++++++- telegram/chat.py | 24 +++++++++++++- telegram/constants.py | 3 +- telegram/message.py | 27 ++++++++++++--- telegram/messageid.py | 3 +- telegram/poll.py | 13 +++++--- telegram/user.py | 24 +++++++++++++- tests/test_bot.py | 66 ++++++++++++++++++++++++++++++++++++- tests/test_callbackquery.py | 13 ++++++++ tests/test_chat.py | 18 ++++++++++ tests/test_message.py | 24 +++++++++++++- tests/test_messageid.py | 53 +++++++++++++++++++++++++++++ tests/test_user.py | 18 ++++++++++ 14 files changed, 289 insertions(+), 19 deletions(-) create mode 100644 tests/test_messageid.py diff --git a/telegram/bot.py b/telegram/bot.py index c9e7694b240..e328c3279d8 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -96,7 +96,7 @@ from telegram.error import InvalidToken, TelegramError from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, to_timestamp from telegram.utils.request import Request -from telegram.utils.types import FileLike, JSONDict, SLT +from telegram.utils.types import FileLike, JSONDict if TYPE_CHECKING: from telegram.ext import Defaults @@ -4381,7 +4381,7 @@ def copy_message( message_id: Union[str, int], caption: str = None, parse_mode: str = None, - caption_entities: SLT[MessageEntity] = None, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, disable_notification: bool = False, reply_to_message_id: Union[int, str] = None, allow_sending_without_reply: bool = False, diff --git a/telegram/callbackquery.py b/telegram/callbackquery.py index f47b1f5f4f2..341acca9875 100644 --- a/telegram/callbackquery.py +++ b/telegram/callbackquery.py @@ -24,7 +24,7 @@ from telegram.utils.types import JSONDict if TYPE_CHECKING: - from telegram import Bot, GameHighScore, InlineKeyboardMarkup + from telegram import Bot, GameHighScore, InlineKeyboardMarkup, MessageId class CallbackQuery(TelegramObject): @@ -325,3 +325,19 @@ def delete_message(self, *args: Any, **kwargs: Any) -> Union[Message, bool]: """ return self.message.delete(*args, **kwargs) + + def copy(self, chat_id: int, *args: Any, **kwargs: Any) -> 'MessageId': + """Shortcut for:: + + update.callback_query.message.copy( + chat_id, + from_chat_id=update.message.chat_id, + message_id=update.message.message_id, + *args, + **kwargs) + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + return self.message.copy(chat_id, *args, **kwargs) diff --git a/telegram/chat.py b/telegram/chat.py index d1c4c7e481e..1003bb287ff 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -27,7 +27,7 @@ from .chatpermissions import ChatPermissions if TYPE_CHECKING: - from telegram import Bot, ChatMember, Message + from telegram import Bot, ChatMember, Message, MessageId class Chat(TelegramObject): @@ -477,3 +477,25 @@ def send_poll(self, *args: Any, **kwargs: Any) -> 'Message': """ return self.bot.send_poll(self.id, *args, **kwargs) + + def send_copy(self, *args: Any, **kwargs: Any) -> 'MessageId': + """Shortcut for:: + + bot.copy_message(chat_id=update.effective_chat.id, *args, **kwargs) + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.copy_message(chat_id=self.id, *args, **kwargs) + + def copy_message(self, *args: Any, **kwargs: Any) -> 'MessageId': + """Shortcut for:: + + bot.copy_message(from_chat_id=update.effective_chat.id, *args, **kwargs) + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.copy_message(from_chat_id=self.id, *args, **kwargs) diff --git a/telegram/constants.py b/telegram/constants.py index 65760a014fe..d71cdc7fea7 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -224,7 +224,8 @@ POLL_REGULAR: str = 'regular' POLL_QUIZ: str = 'quiz' -POLL_QUESTION_LENGTH: int = 300 +MAX_POLL_QUESTION_LENGTH: int = 300 +MAX_POLL_OPTION_LENGTH: int = 100 STICKER_FOREHEAD: str = 'forehead' STICKER_EYES: str = 'eyes' diff --git a/telegram/message.py b/telegram/message.py index 38fbcf9cab0..6dd76ee6921 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -928,10 +928,10 @@ def copy(self, chat_id: int, *args: Any, **kwargs: Any) -> 'MessageId': """Shortcut for:: bot.copy_message(chat_id=chat_id, - from_chat_id=update.message.chat_id, - message_id=update.message.message_id, - *args, - **kwargs) + from_chat_id=update.message.chat_id, + message_id=update.message.message_id, + *args, + **kwargs) Returns: :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. @@ -941,6 +941,25 @@ def copy(self, chat_id: int, *args: Any, **kwargs: Any) -> 'MessageId': chat_id=chat_id, from_chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs ) + def reply_copy( + self, from_chat_id: int, message_id: int, *args: Any, **kwargs: Any + ) -> 'MessageId': + """Shortcut for:: + + bot.copy_message(chat_id=message.chat.id, + from_chat_id=from_chat_id, + message_id=message_id, + *args, + **kwargs) + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + return self.bot.copy_message( + chat_id=self.chat_id, from_chat_id=from_chat_id, message_id=message_id, *args, **kwargs + ) + def edit_text(self, *args: Any, **kwargs: Any) -> Union['Message', bool]: """Shortcut for:: diff --git a/telegram/messageid.py b/telegram/messageid.py index bdc44e8d755..047a3517275 100644 --- a/telegram/messageid.py +++ b/telegram/messageid.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2020 @@ -17,7 +16,7 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -"""This module contains an object that represents a type of a Telegram Poll.""" +"""This module contains an object that represents an instance of a Telegram MessageId.""" from typing import Any from telegram import TelegramObject diff --git a/telegram/poll.py b/telegram/poll.py index 42c2080fe19..ebabe8d7c09 100644 --- a/telegram/poll.py +++ b/telegram/poll.py @@ -39,11 +39,11 @@ class PollOption(TelegramObject): considered equal, if their :attr:`text` and :attr:`voter_count` are equal. Attributes: - text (:obj:`str`): Option text, 1-300 characters. + text (:obj:`str`): Option text, 1-100 characters. voter_count (:obj:`int`): Number of users that voted for this option. Args: - text (:obj:`str`): Option text, 1-300 characters. + text (:obj:`str`): Option text, 1-100 characters. voter_count (:obj:`int`): Number of users that voted for this option. """ @@ -54,6 +54,9 @@ def __init__(self, text: str, voter_count: int, **_kwargs: Any): self._id_attrs = (self.text, self.voter_count) + MAX_POLL_OPTION_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH + """:const:`telegram.constants.POLL_QUESTION_LENGTH`""" + class PollAnswer(TelegramObject): """ @@ -103,7 +106,7 @@ class Poll(TelegramObject): Attributes: id (:obj:`str`): Unique poll identifier. - question (:obj:`str`): Poll question, 1-255 characters. + question (:obj:`str`): Poll question, 1-300 characters. options (List[:class:`PollOption`]): List of poll options. total_voter_count (:obj:`int`): Total number of users that voted in the poll. is_closed (:obj:`bool`): :obj:`True`, if the poll is closed. @@ -122,7 +125,7 @@ class Poll(TelegramObject): Args: id (:obj:`str`): Unique poll identifier. - question (:obj:`str`): Poll question, 1-255 characters. + question (:obj:`str`): Poll question, 1-300 characters. options (List[:class:`PollOption`]): List of poll options. is_closed (:obj:`bool`): :obj:`True`, if the poll is closed. is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous. @@ -262,5 +265,5 @@ def parse_explanation_entities(self, types: List[str] = None) -> Dict[MessageEnt """:const:`telegram.constants.POLL_REGULAR`""" QUIZ: ClassVar[str] = constants.POLL_QUIZ """:const:`telegram.constants.POLL_QUIZ`""" - POLL_QUESTION_LENGTH: ClassVar[int] = constants.POLL_QUESTION_LENGTH + MAX_POLL_QUESTION_LENGTH: ClassVar[int] = constants.MAX_POLL_QUESTION_LENGTH """:const:`telegram.constants.POLL_QUESTION_LENGTH`""" diff --git a/telegram/user.py b/telegram/user.py index 8f9a93cda2f..fcd9cd94ee9 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -26,7 +26,7 @@ from telegram.utils.helpers import mention_markdown as util_mention_markdown if TYPE_CHECKING: - from telegram import Bot, Message, UserProfilePhotos + from telegram import Bot, Message, UserProfilePhotos, MessageId class User(TelegramObject): @@ -389,3 +389,25 @@ def send_poll(self, *args: Any, **kwargs: Any) -> 'Message': """ return self.bot.send_poll(self.id, *args, **kwargs) + + def send_copy(self, *args: Any, **kwargs: Any) -> 'MessageId': + """Shortcut for:: + + bot.copy_message(chat_id=update.effective_user.id, *args, **kwargs) + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.copy_message(chat_id=self.id, *args, **kwargs) + + def copy_message(self, *args: Any, **kwargs: Any) -> 'MessageId': + """Shortcut for:: + + bot.copy_message(from_chat_id=update.effective_user.id, *args, **kwargs) + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.copy_message(from_chat_id=self.id, *args, **kwargs) diff --git a/tests/test_bot.py b/tests/test_bot.py index 21d52de2bf5..5fc35665086 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1383,7 +1383,9 @@ def test_set_and_get_my_commands_strings(self, bot): @flaky(3, 1) @pytest.mark.timeout(10) def test_copy_message(self, monkeypatch, bot, chat_id, media_message): - keyboard = [[InlineKeyboardButton(text="test", callback_data="test2")]] + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="test", callback_data="test2")]] + ) def post(url, data, timeout): assert data["chat_id"] == chat_id @@ -1405,3 +1407,65 @@ def post(url, data, timeout): reply_to_message_id=media_message.message_id, reply_markup=keyboard, ) + + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_copy_message_without_reply(self, bot, chat_id, media_message): + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="test", callback_data="test2")]] + ) + + returned = bot.copy_message( + chat_id, + from_chat_id=chat_id, + message_id=media_message.message_id, + caption="Test", + parse_mode=ParseMode.HTML, + reply_to_message_id=media_message.message_id, + reply_markup=keyboard, + ) + # we send a temp message which replies to the returned message id in order to get a + # message object + temp_message = bot.send_message(chat_id, "test", reply_to_message_id=returned.message_id) + message = temp_message.reply_to_message + assert message.chat_id == int(chat_id) + assert message.caption == "Test" + assert len(message.caption_entities) == 1 + assert message.reply_markup == keyboard + + """#TODO: Add to test + ({'allow_sending_without_reply': True}, None), + ({'allow_sending_without_reply': False}, None), + ({'allow_sending_without_reply': False}, True), + """ + + @flaky(3, 1) + @pytest.mark.timeout(10) + @pytest.mark.parametrize( + 'default_bot,custom', + [ + ({'parse_mode': ParseMode.HTML}, None), + ({'parse_mode': False}, None), + ({'parse_mode': False}, True), + ], + indirect=['default_bot'], + ) + def test_copy_message_with_default(self, default_bot, chat_id, media_message, custom): + returned = default_bot.copy_message( + chat_id, + from_chat_id=chat_id, + message_id=media_message.message_id, + caption="Test", + ) + # we send a temp message which replies to the returned message id in order to get a + # message object + temp_message = default_bot.send_message( + chat_id, "test", reply_to_message_id=returned.message_id + ) + message = temp_message.reply_to_message + if custom is not None: + assert len(message.caption_entities) == 0 + elif default_bot.defaults.parse_mode: + assert len(message.caption_entities) == 1 + else: + assert len(message.caption_entities) == 0 diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 463718b1e0e..e4eba843edc 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -223,6 +223,19 @@ def make_assertion(*args, **kwargs): monkeypatch.setattr(callback_query.bot, 'delete_message', make_assertion) assert callback_query.delete_message() + def test_copy_message(self, monkeypatch, callback_query): + if callback_query.inline_message_id: + pytest.skip("Can't copy inline messages") + + def make_assertion(*args, **kwargs): + id_ = kwargs['from_chat_id'] == callback_query.message.chat_id + chat_id = kwargs['chat_id'] == 1 + message = kwargs['message_id'] == callback_query.message.message_id + return id_ and message and chat_id + + monkeypatch.setattr(callback_query.bot, 'copy_message', make_assertion) + assert callback_query.copy(1) + def test_equality(self): a = CallbackQuery(self.id_, self.from_user, 'chat') b = CallbackQuery(self.id_, self.from_user, 'chat') diff --git a/tests/test_chat.py b/tests/test_chat.py index 337c209c8bc..a08f2fb924e 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -302,6 +302,24 @@ def test(*args, **kwargs): monkeypatch.setattr(chat.bot, 'send_poll', test) assert chat.send_poll('test_poll') + def test_instance_method_send_copy(self, monkeypatch, chat): + def test(*args, **kwargs): + assert args[0] == 'test_copy' + assert kwargs['chat_id'] == chat.id + return args + + monkeypatch.setattr(chat.bot, 'copy_message', test) + assert chat.send_copy('test_copy') + + def test_instance_method_copy_message(self, monkeypatch, chat): + def test(*args, **kwargs): + assert args[0] == 'test_copy' + assert kwargs['from_chat_id'] == chat.id + return args + + monkeypatch.setattr(chat.bot, 'copy_message', test) + assert chat.copy_message('test_copy') + def test_equality(self): a = Chat(self.id_, self.title, self.type_) b = Chat(self.id_, self.title, self.type_) diff --git a/tests/test_message.py b/tests/test_message.py index da3a7582251..2b79983baa0 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -944,7 +944,6 @@ def test(*args, **kwargs): assert message.forward(123456, disable_notification=True) assert not message.forward(635241) - @pytest.mark.test def test_copy(self, monkeypatch, message): keyboard = [[1, 2]] @@ -968,6 +967,29 @@ def test(*args, **kwargs): assert message.copy(123456, reply_markup=keyboard) assert not message.copy(635241) + @pytest.mark.pfff + def test_reply_copy(self, monkeypatch, message): + keyboard = [[1, 2]] + + def test(*args, **kwargs): + chat_id = kwargs['from_chat_id'] == 123456 + from_chat = kwargs['chat_id'] == message.chat_id + message_id = kwargs['message_id'] == 456789 + if kwargs.get('disable_notification'): + notification = kwargs['disable_notification'] is True + else: + notification = True + if kwargs.get('reply_markup'): + reply_markup = kwargs['reply_markup'] is keyboard + else: + reply_markup = True + return chat_id and from_chat and message_id and notification and reply_markup + + monkeypatch.setattr(message.bot, 'copy_message', test) + assert message.reply_copy(123456, 456789) + assert message.reply_copy(123456, 456789, disable_notification=True) + assert message.reply_copy(123456, 456789, reply_markup=keyboard) + def test_edit_text(self, monkeypatch, message): def test(*args, **kwargs): chat_id = kwargs['chat_id'] == message.chat_id diff --git a/tests/test_messageid.py b/tests/test_messageid.py new file mode 100644 index 00000000000..44586e4bc38 --- /dev/null +++ b/tests/test_messageid.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2020 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +import pytest +from telegram import MessageId + + +@pytest.fixture(scope="class") +def message_id(): + return MessageId(message_id=TestMessageId.m_id) + + +class TestMessageId: + m_id = 1234 + + def test_de_json(self): + json_dict = {'message_id': self.m_id} + message_id = MessageId.de_json(json_dict, None) + + assert message_id.message_id == self.m_id + + def test_to_dict(self, message_id): + message_id_dict = message_id.to_dict() + + assert isinstance(message_id_dict, dict) + assert message_id_dict['message_id'] == message_id.message_id + + def test_equality(self): + a = MessageId(message_id=1) + b = MessageId(message_id=1) + c = MessageId(message_id=2) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) diff --git a/tests/test_user.py b/tests/test_user.py index 15fc159df6a..5c15fbfa88c 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -270,6 +270,24 @@ def test(*args, **kwargs): monkeypatch.setattr(user.bot, 'send_poll', test) assert user.send_poll('test_poll') + def test_instance_method_send_copy(self, monkeypatch, user): + def test(*args, **kwargs): + assert args[0] == 'test_copy' + assert kwargs['chat_id'] == user.id + return args + + monkeypatch.setattr(user.bot, 'copy_message', test) + assert user.send_copy('test_copy') + + def test_instance_method_copy_message(self, monkeypatch, user): + def test(*args, **kwargs): + assert args[0] == 'test_copy' + assert kwargs['from_chat_id'] == user.id + return args + + monkeypatch.setattr(user.bot, 'copy_message', test) + assert user.copy_message('test_copy') + def test_mention_html(self, user): expected = u'{}' From 762e9f7ac2c372e00b0678130ffb4e80b258655b Mon Sep 17 00:00:00 2001 From: Poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Wed, 18 Nov 2020 17:39:04 +0100 Subject: [PATCH 3/6] Apply suggestions from Bibo-Joshi Co-authored-by: Bibo-Joshi --- telegram/callbackquery.py | 2 +- tests/test_bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/telegram/callbackquery.py b/telegram/callbackquery.py index 61238922a78..f9e02a10554 100644 --- a/telegram/callbackquery.py +++ b/telegram/callbackquery.py @@ -354,7 +354,7 @@ def unpin_message(self, *args: Any, **kwargs: Any) -> bool: """ return self.message.unpin(*args, **kwargs) - def copy(self, chat_id: int, *args: Any, **kwargs: Any) -> 'MessageId': + def copy_message(self, chat_id: int, *args: Any, **kwargs: Any) -> 'MessageId': """Shortcut for:: update.callback_query.message.copy( diff --git a/tests/test_bot.py b/tests/test_bot.py index b0498301f61..fb8b9be8ac7 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1588,7 +1588,7 @@ def post(url, data, timeout): assert data["caption"] == "Test" assert data["parse_mode"] == ParseMode.HTML assert data["reply_to_message_id"] == media_message.message_id - assert data["reply_markup"] == keyboard + assert data["reply_markup"] == keyboard.to_dict() return data monkeypatch.setattr(bot.request, 'post', post) From cfecff53f8faed4aac872d08429394441f87ac1e Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Wed, 18 Nov 2020 18:09:14 +0100 Subject: [PATCH 4/6] addressing review comments V2 --- telegram/constants.py | 3 ++- telegram/messageid.py | 4 ++-- telegram/poll.py | 6 ++++-- tests/test_bot.py | 42 ++++++++++++++++++++++++++--------------- tests/test_messageid.py | 6 +++++- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/telegram/constants.py b/telegram/constants.py index d71cdc7fea7..c8ff6cb5cae 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -119,7 +119,8 @@ Attributes: POLL_REGULAR (:obj:`str`): 'regular' POLL_QUIZ (:obj:`str`): 'quiz' - POLL_QUESTION_LENGTH (:obj:`int`): 300 + MAX_POLL_QUESTION_LENGTH (:obj:`int`): 300 + MAX_POLL_OPTION_LENGTH (:obj:`int`): 100 :class:`telegram.files.MaskPosition`: diff --git a/telegram/messageid.py b/telegram/messageid.py index 047a3517275..9614da9611f 100644 --- a/telegram/messageid.py +++ b/telegram/messageid.py @@ -32,7 +32,7 @@ class MessageId(TelegramObject): message_id (:obj:`int`): Unique message identifier """ - def __init__(self, message_id: int, **_kwargs: Any): # pylint: disable=W0622 - self.message_id = message_id + def __init__(self, message_id: int, **_kwargs: Any): + self.message_id = int(message_id) self._id_attrs = (self.message_id,) diff --git a/telegram/poll.py b/telegram/poll.py index ebabe8d7c09..79a3db19037 100644 --- a/telegram/poll.py +++ b/telegram/poll.py @@ -54,7 +54,7 @@ def __init__(self, text: str, voter_count: int, **_kwargs: Any): self._id_attrs = (self.text, self.voter_count) - MAX_POLL_OPTION_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH + MAX_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH """:const:`telegram.constants.POLL_QUESTION_LENGTH`""" @@ -265,5 +265,7 @@ def parse_explanation_entities(self, types: List[str] = None) -> Dict[MessageEnt """:const:`telegram.constants.POLL_REGULAR`""" QUIZ: ClassVar[str] = constants.POLL_QUIZ """:const:`telegram.constants.POLL_QUIZ`""" - MAX_POLL_QUESTION_LENGTH: ClassVar[int] = constants.MAX_POLL_QUESTION_LENGTH + MAX_QUESTION_LENGTH: ClassVar[int] = constants.MAX_POLL_QUESTION_LENGTH + """:const:`telegram.constants.POLL_QUESTION_LENGTH`""" + MAX_OPTION_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH """:const:`telegram.constants.POLL_QUESTION_LENGTH`""" diff --git a/tests/test_bot.py b/tests/test_bot.py index fb8b9be8ac7..b87f479d96d 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1588,7 +1588,7 @@ def post(url, data, timeout): assert data["caption"] == "Test" assert data["parse_mode"] == ParseMode.HTML assert data["reply_to_message_id"] == media_message.message_id - assert data["reply_markup"] == keyboard.to_dict() + assert data["reply_markup"] == keyboard.to_json() return data monkeypatch.setattr(bot.request, 'post', post) @@ -1636,30 +1636,42 @@ def test_copy_message_without_reply(self, bot, chat_id, media_message): @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize( - 'default_bot,custom', + 'default_bot', [ - ({'parse_mode': ParseMode.HTML}, None), - ({'parse_mode': False}, None), - ({'parse_mode': False}, True), + ({'parse_mode': ParseMode.HTML, 'allow_sending_without_reply': True}), + ({'parse_mode': False, 'allow_sending_without_reply': True}), + ({'parse_mode': False, 'allow_sending_without_reply': False}), ], indirect=['default_bot'], ) - def test_copy_message_with_default(self, default_bot, chat_id, media_message, custom): - returned = default_bot.copy_message( - chat_id, - from_chat_id=chat_id, - message_id=media_message.message_id, - caption="Test", - ) + def test_copy_message_with_default(self, default_bot, chat_id, media_message): + reply_to_message = default_bot.send_message(chat_id, 'test') + reply_to_message.delete() + if not default_bot.defaults.allow_sending_without_reply: + with pytest.raises(BadRequest, match='Reply message not found'): + default_bot.copy_message( + chat_id, + from_chat_id=chat_id, + message_id=media_message.message_id, + caption="Test", + reply_to_message_id=reply_to_message.message_id, + ) + return + else: + returned = default_bot.copy_message( + chat_id, + from_chat_id=chat_id, + message_id=media_message.message_id, + caption="Test", + reply_to_message_id=reply_to_message.message_id, + ) # we send a temp message which replies to the returned message id in order to get a # message object temp_message = default_bot.send_message( chat_id, "test", reply_to_message_id=returned.message_id ) message = temp_message.reply_to_message - if custom is not None: - assert len(message.caption_entities) == 0 - elif default_bot.defaults.parse_mode: + if default_bot.defaults.parse_mode: assert len(message.caption_entities) == 1 else: assert len(message.caption_entities) == 0 diff --git a/tests/test_messageid.py b/tests/test_messageid.py index 44586e4bc38..326c36255a9 100644 --- a/tests/test_messageid.py +++ b/tests/test_messageid.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. import pytest -from telegram import MessageId +from telegram import MessageId, User @pytest.fixture(scope="class") @@ -45,9 +45,13 @@ def test_equality(self): a = MessageId(message_id=1) b = MessageId(message_id=1) c = MessageId(message_id=2) + d = User(id=1, first_name='name', is_bot=False) assert a == b assert hash(a) == hash(b) assert a != c assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) From 5e30e2e061638a98318f00d4bc1c5f9e70042f1a Mon Sep 17 00:00:00 2001 From: Poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Fri, 20 Nov 2020 15:40:55 +0100 Subject: [PATCH 5/6] Apply suggestions from Hinrich Co-authored-by: Bibo-Joshi --- telegram/poll.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telegram/poll.py b/telegram/poll.py index 79a3db19037..ae2bfa31818 100644 --- a/telegram/poll.py +++ b/telegram/poll.py @@ -55,7 +55,7 @@ def __init__(self, text: str, voter_count: int, **_kwargs: Any): self._id_attrs = (self.text, self.voter_count) MAX_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH - """:const:`telegram.constants.POLL_QUESTION_LENGTH`""" + """:const:`telegram.constants.MAX_POLL_OPTION_LENGTH`""" class PollAnswer(TelegramObject): @@ -266,6 +266,6 @@ def parse_explanation_entities(self, types: List[str] = None) -> Dict[MessageEnt QUIZ: ClassVar[str] = constants.POLL_QUIZ """:const:`telegram.constants.POLL_QUIZ`""" MAX_QUESTION_LENGTH: ClassVar[int] = constants.MAX_POLL_QUESTION_LENGTH - """:const:`telegram.constants.POLL_QUESTION_LENGTH`""" + """:const:`telegram.constants.MAX_POLL_QUESTION_LENGTH`""" MAX_OPTION_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH - """:const:`telegram.constants.POLL_QUESTION_LENGTH`""" + """:const:`telegram.constants.MAX_POLL_OPTION_LENGTH`""" From eb720c9face65eac020f2d81aaa368cc1c63b3b1 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 20 Nov 2020 15:51:01 +0100 Subject: [PATCH 6/6] Fix tests --- tests/test_callbackquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 8e06f3c2743..686ce2b9923 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -262,7 +262,7 @@ def make_assertion(*args, **kwargs): return id_ and message and chat_id monkeypatch.setattr(callback_query.bot, 'copy_message', make_assertion) - assert callback_query.copy(1) + assert callback_query.copy_message(1) def test_equality(self): a = CallbackQuery(self.id_, self.from_user, 'chat')