diff --git a/telegram/bot.py b/telegram/bot.py index daad48e5722..6ba072824f4 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -3628,18 +3628,18 @@ def pin_chat_message( api_kwargs: JSONDict = None, ) -> bool: """ - Use this method to pin a message in a group, a supergroup, or a channel. - The bot must be an administrator in the chat for this to work and must have the - ‘can_pin_messages’ admin right in the supergroup or ‘can_edit_messages’ admin right - in the channel. + Use this method to add a message to the list of pinned messages in a chat. If the + chat is not a private chat, the bot must be an administrator in the chat for this to work + and must have the ``can_pin_messages`` admin right in a supergroup or ``can_edit_messages`` + admin right in a channel. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). message_id (:obj:`int`): Identifier of a message to pin. disable_notification (:obj:`bool`, optional): Pass :obj:`True`, if it is not necessary - to send a notification to all group members about the new pinned message. - Notifications are always disabled in channels. + to send a notification to all chat members about the new pinned message. + Notifications are always disabled in channels and private chats. 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). @@ -3658,23 +3658,29 @@ def pin_chat_message( if disable_notification is not None: data['disable_notification'] = disable_notification - result = self._post('pinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs) - - return result # type: ignore[return-value] + return self._post( # type: ignore[return-value] + 'pinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs + ) @log def unpin_chat_message( - self, chat_id: Union[str, int], timeout: float = None, api_kwargs: JSONDict = None + self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None, + message_id: Union[str, int] = None, ) -> bool: """ - Use this method to unpin a message in a group, a supergroup, or a channel. - The bot must be an administrator in the chat for this to work and must have the - ``can_pin_messages`` admin right in the supergroup or ``can_edit_messages`` admin right - in the channel. + Use this method to remove a message from the list of pinned messages in a chat. If the + chat is not a private chat, the bot must be an administrator in the chat for this to work + and must have the ``can_pin_messages`` admin right in a supergroup or ``can_edit_messages`` + admin right in a channel. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). + message_id (:obj:`int`, optional): Identifier of a message to unpin. If not specified, + the most recent pinned message (by sending date) will be unpinned. 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). @@ -3690,9 +3696,48 @@ def unpin_chat_message( """ data: JSONDict = {'chat_id': chat_id} - result = self._post('unpinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs) + if message_id is not None: + data['message_id'] = message_id + + return self._post( # type: ignore[return-value] + 'unpinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs + ) - return result # type: ignore[return-value] + @log + def unpin_all_chat_messages( + self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to clear the list of pinned messages in a chat. If the + chat is not a private chat, the bot must be an administrator in the chat for this + to work and must have the ``can_pin_messages`` admin right in a supergroup or + ``can_edit_messages`` admin right in a channel. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format @channelusername). + 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: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.TelegramError` + + """ + + data: JSONDict = {'chat_id': chat_id} + + return self._post( # type: ignore[return-value] + 'unpinAllChatMessages', data, timeout=timeout, api_kwargs=api_kwargs + ) @log def get_sticker_set( @@ -4483,6 +4528,8 @@ def to_dict(self) -> JSONDict: """Alias for :attr:`pin_chat_message`""" unpinChatMessage = unpin_chat_message """Alias for :attr:`unpin_chat_message`""" + unpinAllChatMessages = unpin_all_chat_messages + """Alias for :attr:`unpin_all_chat_messages`""" getStickerSet = get_sticker_set """Alias for :attr:`get_sticker_set`""" uploadStickerFile = upload_sticker_file diff --git a/telegram/callbackquery.py b/telegram/callbackquery.py index f47b1f5f4f2..98c3503d79b 100644 --- a/telegram/callbackquery.py +++ b/telegram/callbackquery.py @@ -325,3 +325,31 @@ def delete_message(self, *args: Any, **kwargs: Any) -> Union[Message, bool]: """ return self.message.delete(*args, **kwargs) + + def pin_message(self, *args: Any, **kwargs: Any) -> bool: + """Shortcut for:: + + bot.pin_chat_message(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.message.pin(*args, **kwargs) + + def unpin_message(self, *args: Any, **kwargs: Any) -> bool: + """Shortcut for:: + + bot.unpin_chat_message(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.message.unpin(*args, **kwargs) diff --git a/telegram/chat.py b/telegram/chat.py index d1c4c7e481e..bc8e975eb31 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -46,8 +46,8 @@ class Chat(TelegramObject): photo (:class:`telegram.ChatPhoto`): Optional. Chat photo. description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats. invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats. - pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups. - Returned only in :meth:`telegram.Bot.get_chat`. + pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message + (by sending date). Returned only in :meth:`telegram.Bot.get_chat`. permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`. slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between @@ -77,8 +77,8 @@ class Chat(TelegramObject): chats. Each administrator in a chat generates their own invite links, so the bot must first generate the link using ``export_chat_invite_link()``. Returned only in :meth:`telegram.Bot.get_chat`. - pinned_message (:class:`telegram.Message`, optional): Pinned message, for groups, - supergroups and channels. Returned only in :meth:`telegram.Bot.get_chat`. + pinned_message (:class:`telegram.Message`, optional): The most recent pinned message + (by sending date). Returned only in :meth:`telegram.Bot.get_chat`. permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`. slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between @@ -277,6 +277,45 @@ def set_administrator_custom_title(self, *args: Any, **kwargs: Any) -> bool: """ return self.bot.set_chat_administrator_custom_title(self.id, *args, **kwargs) + def pin_message(self, *args: Any, **kwargs: Any) -> bool: + """Shortcut for:: + + bot.pin_chat_message(chat_id=update.effective_chat.id, + *args, + **kwargs) + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.pin_chat_message(self.id, *args, **kwargs) + + def unpin_message(self, *args: Any, **kwargs: Any) -> bool: + """Shortcut for:: + + bot.unpin_chat_message(chat_id=update.effective_chat.id, + *args, + **kwargs) + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_chat_message(self.id, *args, **kwargs) + + def unpin_all_messages(self, *args: Any, **kwargs: Any) -> bool: + """Shortcut for:: + + bot.unpin_all_chat_messages(chat_id=update.effective_chat.id, + *args, + **kwargs) + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_all_chat_messages(chat_id=self.id, *args, **kwargs) + def send_message(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: diff --git a/telegram/message.py b/telegram/message.py index 7f2d2ab9eee..0e5cfda3e2e 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -1136,13 +1136,29 @@ def pin(self, *args: Any, **kwargs: Any) -> bool: **kwargs) Returns: - :obj:`True`: On success. + :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.pin_chat_message( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs ) + def unpin(self, *args: Any, **kwargs: Any) -> bool: + """Shortcut for:: + + bot.unpin_chat_message(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_chat_message( + chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs + ) + def parse_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. diff --git a/telegram/user.py b/telegram/user.py index 8f9a93cda2f..cf24e528142 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -189,6 +189,45 @@ def mention_html(self, name: str = None) -> str: return util_mention_html(self.id, name) return util_mention_html(self.id, self.full_name) + def pin_message(self, *args: Any, **kwargs: Any) -> bool: + """Shortcut for:: + + bot.pin_chat_message(chat_id=update.effective_user.id, + *args, + **kwargs) + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.pin_chat_message(self.id, *args, **kwargs) + + def unpin_message(self, *args: Any, **kwargs: Any) -> bool: + """Shortcut for:: + + bot.unpin_chat_message(chat_id=update.effective_user.id, + *args, + **kwargs) + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_chat_message(self.id, *args, **kwargs) + + def unpin_all_messages(self, *args: Any, **kwargs: Any) -> bool: + """Shortcut for:: + + bot.unpin_all_chat_messages(chat_id=update.effective_user.id, + *args, + **kwargs) + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_all_chat_messages(self.id, *args, **kwargs) + def send_message(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: diff --git a/tests/test_bot.py b/tests/test_bot.py index f12e51671fe..acecd4657fd 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1263,15 +1263,28 @@ def test_set_chat_description(self, bot, channel_id): @flaky(3, 1) @pytest.mark.timeout(10) def test_pin_and_unpin_message(self, bot, super_group_id): - message = bot.send_message(super_group_id, text="test_pin_message") + message1 = bot.send_message(super_group_id, text="test_pin_message_1") + message2 = bot.send_message(super_group_id, text="test_pin_message_2") + message3 = bot.send_message(super_group_id, text="test_pin_message_3") + assert bot.pin_chat_message( - chat_id=super_group_id, message_id=message.message_id, disable_notification=True + chat_id=super_group_id, message_id=message1.message_id, disable_notification=True + ) + + bot.pin_chat_message( + chat_id=super_group_id, message_id=message2.message_id, disable_notification=True + ) + bot.pin_chat_message( + chat_id=super_group_id, message_id=message3.message_id, disable_notification=True ) chat = bot.get_chat(super_group_id) - assert chat.pinned_message == message + assert chat.pinned_message == message3 + + assert bot.unpin_chat_message(super_group_id, message_id=message2.message_id) + assert bot.unpin_chat_message(super_group_id) - assert bot.unpinChatMessage(super_group_id) + assert bot.unpin_all_chat_messages(super_group_id) # get_sticker_set, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, # set_sticker_position_in_set and delete_sticker_from_set are tested in the diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 463718b1e0e..0c2d0c9ebf9 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -223,6 +223,34 @@ def make_assertion(*args, **kwargs): monkeypatch.setattr(callback_query.bot, 'delete_message', make_assertion) assert callback_query.delete_message() + def test_pin_message(self, monkeypatch, callback_query): + if callback_query.inline_message_id: + pytest.skip("Can't pin inline messages") + + def make_assertion(*args, **kwargs): + _id = callback_query.message.chat_id + try: + return kwargs['chat_id'] == _id + except KeyError: + return args[0] == _id + + monkeypatch.setattr(callback_query.bot, 'pin_chat_message', make_assertion) + assert callback_query.pin_message() + + def test_unpin_message(self, monkeypatch, callback_query): + if callback_query.inline_message_id: + pytest.skip("Can't unpin inline messages") + + def make_assertion(*args, **kwargs): + _id = callback_query.message.chat_id + try: + return kwargs['chat_id'] == _id + except KeyError: + return args[0] == _id + + monkeypatch.setattr(callback_query.bot, 'unpin_chat_message', make_assertion) + assert callback_query.unpin_message() + 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..160690b0779 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -183,6 +183,36 @@ def test(*args, **kwargs): monkeypatch.setattr('telegram.Bot.set_chat_administrator_custom_title', test) assert chat.set_administrator_custom_title(42, 'custom_title') + def test_pin_message(self, monkeypatch, chat): + def make_assertion(*args, **kwargs): + try: + return kwargs['chat_id'] == chat.id + except KeyError: + return args[0] == chat.id + + monkeypatch.setattr(chat.bot, 'pin_chat_message', make_assertion) + assert chat.pin_message() + + def test_unpin_message(self, monkeypatch, chat): + def make_assertion(*args, **kwargs): + try: + return kwargs['chat_id'] == chat.id + except KeyError: + return args[0] == chat.id + + monkeypatch.setattr(chat.bot, 'unpin_chat_message', make_assertion) + assert chat.unpin_message() + + def test_unpin_all_messages(self, monkeypatch, chat): + def make_assertion(*args, **kwargs): + try: + return kwargs['chat_id'] == chat.id + except KeyError: + return args[0] == chat.id + + monkeypatch.setattr(chat.bot, 'unpin_all_chat_messages', make_assertion) + assert chat.unpin_all_messages() + def test_instance_method_send_message(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test' diff --git a/tests/test_message.py b/tests/test_message.py index ab25f4da5f5..c5b30e01590 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1044,14 +1044,23 @@ def test(*args, **kwargs): assert message.stop_poll() def test_pin(self, monkeypatch, message): - def test(*args, **kwargs): + def make_assertion(*args, **kwargs): chat_id = kwargs['chat_id'] == message.chat_id message_id = kwargs['message_id'] == message.message_id return chat_id and message_id - monkeypatch.setattr(message.bot, 'pin_chat_message', test) + monkeypatch.setattr(message.bot, 'pin_chat_message', make_assertion) assert message.pin() + def test_unpin(self, monkeypatch, message): + def make_assertion(*args, **kwargs): + chat_id = kwargs['chat_id'] == message.chat_id + message_id = kwargs['message_id'] == message.message_id + return chat_id and message_id + + monkeypatch.setattr(message.bot, 'unpin_chat_message', make_assertion) + assert message.unpin() + def test_default_quote(self, message): message.bot.defaults = Defaults() kwargs = {} diff --git a/tests/test_user.py b/tests/test_user.py index 15fc159df6a..1207ad1dd05 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -144,6 +144,27 @@ def test(*args, **kwargs): monkeypatch.setattr(user.bot, 'get_user_profile_photos', test) assert user.get_profile_photos() + def test_pin_message(self, monkeypatch, user): + def make_assertion(*args, **kwargs): + return args[0] == user.id + + monkeypatch.setattr(user.bot, 'pin_chat_message', make_assertion) + assert user.pin_message() + + def test_unpin_message(self, monkeypatch, user): + def make_assertion(*args, **kwargs): + return args[0] == user.id + + monkeypatch.setattr(user.bot, 'unpin_chat_message', make_assertion) + assert user.unpin_message() + + def test_unpin_all_messages(self, monkeypatch, user): + def make_assertion(*args, **kwargs): + return args[0] == user.id + + monkeypatch.setattr(user.bot, 'unpin_all_chat_messages', make_assertion) + assert user.unpin_all_messages() + def test_instance_method_send_message(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test'