diff --git a/README.rst b/README.rst index ee8ebc941ee..ed01ad61723 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ .. - Make user to apply any changes to this file to README_RAW.rst as well! + Make sure to apply any changes to this file to README_RAW.rst as well! .. image:: https://github.com/python-telegram-bot/logos/blob/master/logo-text/png/ptb-logo-text_768.png?raw=true :align: center @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-5.4-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-5.5-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -111,7 +111,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **5.4** are supported. +All types and methods of the Telegram Bot API **5.5** are supported. ========== Installing diff --git a/README_RAW.rst b/README_RAW.rst index a76584e8341..bf78a40ea4b 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -1,5 +1,5 @@ .. - Make user to apply any changes to this file to README.rst as well! + Make sure to apply any changes to this file to README.rst as well! .. image:: https://github.com/python-telegram-bot/logos/blob/master/logo-text/png/ptb-raw-logo-text_768.png?raw=true :align: center @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-5.4-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-5.5-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -105,7 +105,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **5.4** are supported. +All types and methods of the Telegram Bot API **5.5** are supported. ========== Installing diff --git a/telegram/bot.py b/telegram/bot.py index 5051482a1f3..442b2a0a812 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -579,6 +579,14 @@ def forward_message( ) -> Message: """Use this method to forward messages of any kind. Service messages can't be forwarded. + Note: + Since the release of Bot API 5.5 it can be impossible to forward messages from + some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and + :attr:`telegram.Chat.has_protected_content` to check this. + + As a workaround, it is still possible to use :meth:`copy_message`. However, this + behaviour is undocumented and might be changed by Telegram. + Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). @@ -2408,6 +2416,45 @@ def ban_chat_member( return result # type: ignore[return-value] + @log + def ban_chat_sender_chat( + self, + chat_id: Union[str, int], + sender_chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to ban a channel chat in a supergroup or a channel. Until the chat is + unbanned, the owner of the banned chat won't be able to send messages on behalf of **any of + their channels**. The bot must be an administrator in the supergroup or channel for this + to work and must have the appropriate administrator rights. + + .. versionadded:: 13.9 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username + of the target supergroup or channel (in the format ``@channelusername``). + sender_chat_id (:obj:`int`): Unique identifier of the target sender chat. + 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.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'sender_chat_id': sender_chat_id} + + result = self._post('banChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + @log def unban_chat_member( self, @@ -2437,7 +2484,7 @@ def unban_chat_member( Telegram API. Returns: - :obj:`bool` On success, :obj:`True` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` @@ -2452,6 +2499,43 @@ def unban_chat_member( return result # type: ignore[return-value] + @log + def unban_chat_sender_chat( + self, + chat_id: Union[str, int], + sender_chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Use this method to unban a previously banned channel in a supergroup or channel. + The bot must be an administrator for this to work and must have the + appropriate administrator rights. + + .. versionadded:: 13.9 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target supergroup or channel (in the format ``@channelusername``). + sender_chat_id (:obj:`int`): Unique identifier of the target sender chat. + 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.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'sender_chat_id': sender_chat_id} + + result = self._post('unbanChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + @log def answer_callback_query( self, @@ -5499,10 +5583,14 @@ def __hash__(self) -> int: """Alias for :meth:`get_file`""" banChatMember = ban_chat_member """Alias for :meth:`ban_chat_member`""" + banChatSenderChat = ban_chat_sender_chat + """Alias for :meth:`ban_chat_sender_chat`""" kickChatMember = kick_chat_member """Alias for :meth:`kick_chat_member`""" unbanChatMember = unban_chat_member """Alias for :meth:`unban_chat_member`""" + unbanChatSenderChat = unban_chat_sender_chat + """Alias for :meth:`unban_chat_sender_chat`""" answerCallbackQuery = answer_callback_query """Alias for :meth:`answer_callback_query`""" editMessageText = edit_message_text diff --git a/telegram/chat.py b/telegram/chat.py index 33c6a03dd70..0997ac0b688 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -81,6 +81,11 @@ class Chat(TelegramObject): Returned only in :meth:`telegram.Bot.get_chat`. bio (:obj:`str`, optional): Bio of the other party in a private chat. Returned only in :meth:`telegram.Bot.get_chat`. + has_private_forwards (:obj:`bool`, optional): :obj:`True`, if privacy settings of the other + party in the private chat allows to use ``tg://user?id=`` links only in chats + with the user. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.9 description (:obj:`str`, optional): Description, for groups, supergroups and channel chats. Returned only in :meth:`telegram.Bot.get_chat`. invite_link (:obj:`str`, optional): Primary invite link, for groups, supergroups and @@ -97,6 +102,10 @@ class Chat(TelegramObject): :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.4 + has_protected_content (:obj:`bool`, optional): :obj:`True`, if messages from the chat can't + be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.9 bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set. Returned only in :meth:`telegram.Bot.get_chat`. @@ -119,6 +128,11 @@ class Chat(TelegramObject): photo (:class:`telegram.ChatPhoto`): Optional. Chat photo. bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in :meth:`telegram.Bot.get_chat`. + has_private_forwards (:obj:`bool`): Optional. :obj:`True`, if privacy settings of the other + party in the private chat allows to use ``tg://user?id=`` links only in chats + with the user. + + .. versionadded:: 13.9 description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats. invite_link (:obj:`str`): Optional. Primary invite link, for groups, supergroups and channel. Returned only in :meth:`telegram.Bot.get_chat`. @@ -134,6 +148,10 @@ class Chat(TelegramObject): :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.4 + has_protected_content (:obj:`bool`): Optional. :obj:`True`, if messages from the chat can't + be forwarded to other chats. + + .. versionadded:: 13.9 sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the sticker set. @@ -166,6 +184,8 @@ class Chat(TelegramObject): 'linked_chat_id', 'all_members_are_administrators', 'message_auto_delete_time', + 'has_protected_content', + 'has_private_forwards', '_id_attrs', ) @@ -204,6 +224,8 @@ def __init__( linked_chat_id: int = None, location: ChatLocation = None, message_auto_delete_time: int = None, + has_private_forwards: bool = None, + has_protected_content: bool = None, **_kwargs: Any, ): # Required @@ -218,6 +240,7 @@ def __init__( self.all_members_are_administrators = _kwargs.get('all_members_are_administrators') self.photo = photo self.bio = bio + self.has_private_forwards = has_private_forwards self.description = description self.invite_link = invite_link self.pinned_message = pinned_message @@ -226,6 +249,7 @@ def __init__( self.message_auto_delete_time = ( int(message_auto_delete_time) if message_auto_delete_time is not None else None ) + self.has_protected_content = has_protected_content self.sticker_set_name = sticker_set_name self.can_set_sticker_set = can_set_sticker_set self.linked_chat_id = linked_chat_id @@ -433,6 +457,98 @@ def ban_member( revoke_messages=revoke_messages, ) + def ban_sender_chat( + self, + sender_chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.ban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.ban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.ban_chat_sender_chat( + chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs + ) + + def ban_chat( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.ban_chat_sender_chat(sender_chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.ban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.ban_chat_sender_chat( + chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs + ) + + def unban_sender_chat( + self, + sender_chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.unban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unban_chat_sender_chat( + chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs + ) + + def unban_chat( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.unban_chat_sender_chat(sender_chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unban_chat_sender_chat( + chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs + ) + def unban_member( self, user_id: Union[str, int], diff --git a/telegram/chatjoinrequest.py b/telegram/chatjoinrequest.py index 81210c784a9..98c419f45c7 100644 --- a/telegram/chatjoinrequest.py +++ b/telegram/chatjoinrequest.py @@ -34,6 +34,12 @@ class ChatJoinRequest(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`chat`, :attr:`from_user` and :attr:`date` are equal. + Note: + Since Bot API 5.5, bots are allowed to contact users who sent a join request to a chat + where the bot is an administrator with the + :attr:`~telegram.ChatMemberAdministrator.can_invite_users` administrator right – even if + the user never interacted with the bot before. + .. versionadded:: 13.8 Args: diff --git a/telegram/constants.py b/telegram/constants.py index 9179074821b..58e0f946c07 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -21,7 +21,7 @@ `Telegram Bots API `_. Attributes: - BOT_API_VERSION (:obj:`str`): `5.3`. Telegram Bot API version supported by this + BOT_API_VERSION (:obj:`str`): `5.5`. Telegram Bot API version supported by this version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``. .. versionadded:: 13.4 @@ -48,6 +48,10 @@ ANONYMOUS_ADMIN_ID (:obj:`int`): ``1087968824`` (User id in groups for anonymous admin) SERVICE_CHAT_ID (:obj:`int`): ``777000`` (Telegram service chat, that also acts as sender of channel posts forwarded to discussion groups) + FAKE_CHANNEL_ID (:obj:`int`): ``136817688`` (User id in groups when message is sent on behalf + of a channel). + + .. versionadded:: 13.9 The following constants are related to specific classes and are also available as attributes of those classes: @@ -240,11 +244,12 @@ """ from typing import List -BOT_API_VERSION: str = '5.4' +BOT_API_VERSION: str = '5.5' MAX_MESSAGE_LENGTH: int = 4096 MAX_CAPTION_LENGTH: int = 1024 ANONYMOUS_ADMIN_ID: int = 1087968824 SERVICE_CHAT_ID: int = 777000 +FAKE_CHANNEL_ID: int = 136817688 # constants above this line are tested diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 72a4b30f22a..2bb0ed9e30a 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1965,16 +1965,16 @@ def remove_chat_ids(self, chat_id: SLT[int]) -> None: class sender_chat(_ChatUserBaseFilter): # pylint: disable=W0235 - """Filters messages to allow only those which are from a specified sender chats chat ID or + """Filters messages to allow only those which are from a specified sender chat's chat ID or username. Examples: - * To filter for messages forwarded to a discussion group from a channel with ID + * To filter for messages sent to a group by a channel with ID ``-1234``, use ``MessageHandler(Filters.sender_chat(-1234), callback_method)``. * To filter for messages of anonymous admins in a super group with username ``@anonymous``, use ``MessageHandler(Filters.sender_chat(username='anonymous'), callback_method)``. - * To filter for messages forwarded to a discussion group from *any* channel, use + * To filter for messages sent to a group by *any* channel, use ``MessageHandler(Filters.sender_chat.channel, callback_method)``. * To filter for messages of anonymous admins in *any* super group, use ``MessageHandler(Filters.sender_chat.super_group, callback_method)``. @@ -1983,7 +1983,10 @@ class sender_chat(_ChatUserBaseFilter): Remember, ``sender_chat`` is also set for messages in a channel as the channel itself, so when your bot is an admin in a channel and the linked discussion group, you would receive the message twice (once from inside the channel, once inside the discussion - group). + group). Since v13.9, the field :attr:`telegram.Message.is_automatic_forward` will be + :obj:`True` for the discussion group message. + + .. seealso:: :attr:`Filters.is_automatic_forward` Warning: :attr:`chat_ids` will return a *copy* of the saved chat ids as :class:`frozenset`. This @@ -2089,6 +2092,32 @@ def filter(self, message: Message) -> bool: super_group = _SuperGroup() channel = _Channel() + class _IsAutomaticForward(MessageFilter): + __slots__ = () + name = 'Filters.is_automatic_forward' + + def filter(self, message: Message) -> bool: + return bool(message.is_automatic_forward) + + is_automatic_forward = _IsAutomaticForward() + """Messages that contain :attr:`telegram.Message.is_automatic_forward`. + + .. versionadded:: 13.9 + """ + + class _HasProtectedContent(MessageFilter): + __slots__ = () + name = 'Filters.has_protected_content' + + def filter(self, message: Message) -> bool: + return bool(message.has_protected_content) + + has_protected_content = _HasProtectedContent() + """Messages that contain :attr:`telegram.Message.has_protected_content`. + + .. versionadded:: 13.9 + """ + class _Invoice(MessageFilter): __slots__ = () name = 'Filters.invoice' diff --git a/telegram/inline/inlinekeyboardbutton.py b/telegram/inline/inlinekeyboardbutton.py index b9d0c32165a..5aa32cba603 100644 --- a/telegram/inline/inlinekeyboardbutton.py +++ b/telegram/inline/inlinekeyboardbutton.py @@ -45,16 +45,25 @@ class InlineKeyboardButton(TelegramObject): .. versionadded:: 13.6 + * Since Bot API 5.5, it's now allowed to mention users by their ID in inline keyboards. + This will only work in Telegram versions released after December 7, 2021. + Older clients will display *unsupported message*. + Warning: If your bot allows your arbitrary callback data, buttons whose callback data is a - non-hashable object will be come unhashable. Trying to evaluate ``hash(button)`` will + non-hashable object will become unhashable. Trying to evaluate ``hash(button)`` will result in a :class:`TypeError`. .. versionchanged:: 13.6 Args: text (:obj:`str`): Label text on the button. - url (:obj:`str`, optional): HTTP or tg:// url to be opened when button is pressed. + url (:obj:`str`, optional): HTTP or tg:// url to be opened when the button is pressed. + Links ``tg://user?id=`` can be used to mention a user by + their ID without using a username, if this is allowed by their privacy settings. + + .. versionchanged:: 13.9 + You can now mention a user using ``tg://user?id=``. login_url (:class:`telegram.LoginUrl`, optional): An HTTP URL used to automatically authorize the user. Can be used as a replacement for the Telegram Login Widget. callback_data (:obj:`str` | :obj:`Any`, optional): Data to be sent in a callback query to @@ -76,12 +85,18 @@ class InlineKeyboardButton(TelegramObject): be launched when the user presses the button. This type of button must always be the ``first`` button in the first row. pay (:obj:`bool`, optional): Specify :obj:`True`, to send a Pay button. This type of button - must always be the ``first`` button in the first row. + must always be the `first` button in the first row and can only be used in invoice + messages. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: text (:obj:`str`): Label text on the button. - url (:obj:`str`): Optional. HTTP or tg:// url to be opened when button is pressed. + url (:obj:`str`): Optional. HTTP or tg:// url to be opened when the button is pressed. + Links ``tg://user?id=`` can be used to mention a user by + their ID without using a username, if this is allowed by their privacy settings. + + .. versionchanged:: 13.9 + You can now mention a user using ``tg://user?id=``. login_url (:class:`telegram.LoginUrl`): Optional. An HTTP URL used to automatically authorize the user. Can be used as a replacement for the Telegram Login Widget. callback_data (:obj:`str` | :obj:`object`): Optional. Data to be sent in a callback query diff --git a/telegram/message.py b/telegram/message.py index 63e18bf8069..7b439cff237 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -90,12 +90,15 @@ class Message(TelegramObject): Args: message_id (:obj:`int`): Unique message identifier inside this chat. - from_user (:class:`telegram.User`, optional): Sender, empty for messages sent - to channels. + from_user (:class:`telegram.User`, optional): Sender of the message; empty for messages + sent to channels. For backward compatibility, this will contain a fake sender user in + non-channel chats, if the message was sent on behalf of a chat. sender_chat (:class:`telegram.Chat`, optional): Sender of the message, sent on behalf of a - chat. The channel itself for channel messages. The supergroup itself for messages from - anonymous group administrators. The linked channel for messages automatically forwarded - to the discussion group. + chat. For example, the channel itself for channel posts, the supergroup itself for + messages from anonymous group administrators, the linked channel for messages + automatically forwarded to the discussion group. For backward compatibility, + :attr:`from_user` contains a fake sender user in non-channel chats, if the message was + sent on behalf of a chat. date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to :class:`datetime.datetime`. chat (:class:`telegram.Chat`): Conversation the message belongs to. @@ -109,9 +112,17 @@ class Message(TelegramObject): who disallow adding a link to their account in forwarded messages. forward_date (:class:`datetime.datetime`, optional): For forwarded messages, date the original message was sent in Unix time. Converted to :class:`datetime.datetime`. + is_automatic_forward (:obj:`bool`, optional): :obj:`True`, if the message is a channel post + that was automatically forwarded to the connected discussion group. + + .. versionadded:: 13.9 reply_to_message (:class:`telegram.Message`, optional): For replies, the original message. edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix time. Converted to :class:`datetime.datetime`. + has_protected_content (:obj:`bool`, optional): :obj:`True`, if the message can't be + forwarded. + + .. versionadded:: 13.9 media_group_id (:obj:`str`, optional): The unique identifier of a media message group this message belongs to. text (str, optional): For text messages, the actual UTF-8 text of the message, 0-4096 @@ -225,11 +236,12 @@ class Message(TelegramObject): Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. - from_user (:class:`telegram.User`): Optional. Sender. + from_user (:class:`telegram.User`): Optional. Sender of the message; empty for messages + sent to channels. For backward compatibility, this will contain a fake sender user in + non-channel chats, if the message was sent on behalf of a chat. sender_chat (:class:`telegram.Chat`): Optional. Sender of the message, sent on behalf of a - chat. The channel itself for channel messages. The supergroup itself for messages from - anonymous group administrators. The linked channel for messages automatically forwarded - to the discussion group. + chat. For backward compatibility, :attr:`from_user` contains a fake sender user in + non-channel chats, if the message was sent on behalf of a chat. date (:class:`datetime.datetime`): Date the message was sent. chat (:class:`telegram.Chat`): Conversation the message belongs to. forward_from (:class:`telegram.User`): Optional. Sender of the original message. @@ -238,10 +250,18 @@ class Message(TelegramObject): forward_from_message_id (:obj:`int`): Optional. Identifier of the original message in the channel. forward_date (:class:`datetime.datetime`): Optional. Date the original message was sent. + is_automatic_forward (:obj:`bool`): Optional. :obj:`True`, if the message is a channel post + that was automatically forwarded to the connected discussion group. + + .. versionadded:: 13.9 reply_to_message (:class:`telegram.Message`): Optional. For replies, the original message. Note that the Message object in this field will not contain further ``reply_to_message`` fields even if it itself is a reply. edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited. + has_protected_content (:obj:`bool`): Optional. :obj:`True`, if the message can't be + forwarded. + + .. versionadded:: 13.9 media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this message belongs to. text (:obj:`str`): Optional. The actual UTF-8 text of the message. @@ -390,6 +410,8 @@ class Message(TelegramObject): 'voice_chat_participants_invited', 'voice_chat_started', 'voice_chat_scheduled', + 'is_automatic_forward', + 'has_protected_content', '_id_attrs', ) @@ -492,6 +514,8 @@ def __init__( voice_chat_participants_invited: VoiceChatParticipantsInvited = None, message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged = None, voice_chat_scheduled: VoiceChatScheduled = None, + is_automatic_forward: bool = None, + has_protected_content: bool = None, **_kwargs: Any, ): # Required @@ -504,8 +528,10 @@ def __init__( self.forward_from = forward_from self.forward_from_chat = forward_from_chat self.forward_date = forward_date + self.is_automatic_forward = is_automatic_forward self.reply_to_message = reply_to_message self.edit_date = edit_date + self.has_protected_content = has_protected_content self.text = text self.entities = entities or [] self.caption_entities = caption_entities or [] @@ -1795,6 +1821,14 @@ def forward( For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`. + Note: + Since the release of Bot API 5.5 it can be impossible to forward messages from + some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and + :attr:`telegram.Chat.has_protected_content` to check this. + + As a workaround, it is still possible to use :meth:`copy`. However, this + behaviour is undocumented and might be changed by Telegram. + Returns: :class:`telegram.Message`: On success, instance representing the message forwarded. diff --git a/telegram/user.py b/telegram/user.py index 2f7f962ac8c..ce0af46ac36 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union, Tuple from telegram import TelegramObject, constants +from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton from telegram.utils.helpers import ( mention_html as util_mention_html, DEFAULT_NONE, @@ -233,6 +234,22 @@ 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 mention_button(self, name: str = None) -> InlineKeyboardButton: + """ + Shortcut for:: + + InlineKeyboardButton(text=name, url=f"tg://user?id={update.effective_user.id}") + + .. versionadded:: 13.9 + + Args: + name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. + + Returns: + :class:`telegram.InlineKeyboardButton`: InlineButton with url set to the user mention + """ + return InlineKeyboardButton(text=name or self.full_name, url=f"tg://user?id={self.id}") + def pin_message( self, message_id: int, diff --git a/tests/test_bot.py b/tests/test_bot.py index 11eb44e91df..bfda0555f74 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1012,6 +1012,17 @@ def test(url, data, *args, **kwargs): assert tz_bot.ban_chat_member(2, 32, until_date=until) assert tz_bot.ban_chat_member(2, 32, until_date=until_timestamp) + def test_ban_chat_sender_chat(self, monkeypatch, bot): + # For now, we just test that we pass the correct data to TG + def make_assertion(url, data, *args, **kwargs): + chat_id = data['chat_id'] == 2 + sender_chat_id = data['sender_chat_id'] == 32 + return chat_id and sender_chat_id + + monkeypatch.setattr(bot.request, 'post', make_assertion) + assert bot.ban_chat_sender_chat(2, 32) + monkeypatch.delattr(bot.request, 'post') + def test_kick_chat_member_warning(self, monkeypatch, bot, recwarn): def test(url, data, *args, **kwargs): chat_id = data['chat_id'] == 2 @@ -1037,6 +1048,15 @@ def make_assertion(url, data, *args, **kwargs): assert bot.unban_chat_member(2, 32, only_if_banned=only_if_banned) + def test_unban_chat_sender_chat(self, monkeypatch, bot): + def make_assertion(url, data, *args, **kwargs): + chat_id = data['chat_id'] == 2 + sender_chat_id = data['sender_chat_id'] == 32 + return chat_id and sender_chat_id + + monkeypatch.setattr(bot.request, 'post', make_assertion) + assert bot.unbanChatSenderChat(2, 32) + def test_set_chat_permissions(self, monkeypatch, bot, chat_permissions): def test(url, data, *args, **kwargs): chat_id = data['chat_id'] == 2 diff --git a/tests/test_chat.py b/tests/test_chat.py index 7ff7aa39286..00190e7fca7 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -41,6 +41,8 @@ def chat(bot): bio=TestChat.bio, linked_chat_id=TestChat.linked_chat_id, location=TestChat.location, + has_private_forwards=True, + has_protected_content=True, ) @@ -62,6 +64,8 @@ class TestChat: bio = "I'm a Barbie Girl in a Barbie World" linked_chat_id = 11880 location = ChatLocation(Location(123, 456), 'Barbie World') + has_protected_content = True + has_private_forwards = True def test_slot_behaviour(self, chat, recwarn, mro_slots): for attr in chat.__slots__: @@ -84,6 +88,8 @@ def test_de_json(self, bot): 'slow_mode_delay': self.slow_mode_delay, 'message_auto_delete_time': self.message_auto_delete_time, 'bio': self.bio, + 'has_protected_content': self.has_protected_content, + 'has_private_forwards': self.has_private_forwards, 'linked_chat_id': self.linked_chat_id, 'location': self.location.to_dict(), } @@ -100,6 +106,8 @@ def test_de_json(self, bot): assert chat.slow_mode_delay == self.slow_mode_delay assert chat.message_auto_delete_time == self.message_auto_delete_time assert chat.bio == self.bio + assert chat.has_protected_content == self.has_protected_content + assert chat.has_private_forwards == self.has_private_forwards assert chat.linked_chat_id == self.linked_chat_id assert chat.location.location == self.location.location assert chat.location.address == self.location.address @@ -117,6 +125,8 @@ def test_to_dict(self, chat): assert chat_dict['slow_mode_delay'] == chat.slow_mode_delay assert chat_dict['message_auto_delete_time'] == chat.message_auto_delete_time assert chat_dict['bio'] == chat.bio + assert chat_dict['has_private_forwards'] == chat.has_private_forwards + assert chat_dict['has_protected_content'] == chat.has_protected_content assert chat_dict['linked_chat_id'] == chat.linked_chat_id assert chat_dict['location'] == chat.location.to_dict() @@ -225,6 +235,36 @@ def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.bot, 'ban_chat_member', make_assertion) assert chat.ban_member(user_id=42, until_date=43) + def test_ban_sender_chat(self, monkeypatch, chat): + def make_assertion(*_, **kwargs): + chat_id = kwargs['chat_id'] == chat.id + sender_chat_id = kwargs['sender_chat_id'] == 42 + return chat_id and sender_chat_id + + assert check_shortcut_signature( + Chat.ban_sender_chat, Bot.ban_chat_sender_chat, ['chat_id'], [] + ) + assert check_shortcut_call(chat.ban_sender_chat, chat.bot, 'ban_chat_sender_chat') + assert check_defaults_handling(chat.ban_sender_chat, chat.bot) + + monkeypatch.setattr(chat.bot, 'ban_chat_sender_chat', make_assertion) + assert chat.ban_sender_chat(42) + + def test_ban_chat(self, monkeypatch, chat): + def make_assertion(*_, **kwargs): + chat_id = kwargs['chat_id'] == 42 + sender_chat_id = kwargs['sender_chat_id'] == chat.id + return chat_id and sender_chat_id + + assert check_shortcut_signature( + Chat.ban_chat, Bot.ban_chat_sender_chat, ['sender_chat_id'], [] + ) + assert check_shortcut_call(chat.ban_chat, chat.bot, 'ban_chat_sender_chat') + assert check_defaults_handling(chat.ban_chat, chat.bot) + + monkeypatch.setattr(chat.bot, 'ban_chat_sender_chat', make_assertion) + assert chat.ban_chat(42) + def test_kick_member_warning(self, chat, monkeypatch, recwarn): def make_assertion(*_, **kwargs): chat_id = kwargs['chat_id'] == chat.id @@ -252,6 +292,36 @@ def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.bot, 'unban_chat_member', make_assertion) assert chat.unban_member(user_id=42, only_if_banned=only_if_banned) + def test_unban_sender_chat(self, monkeypatch, chat): + def make_assertion(*_, **kwargs): + chat_id = kwargs['chat_id'] == chat.id + sender_chat_id = kwargs['sender_chat_id'] == 42 + return chat_id and sender_chat_id + + assert check_shortcut_signature( + Chat.unban_sender_chat, Bot.unban_chat_sender_chat, ['chat_id'], [] + ) + assert check_shortcut_call(chat.unban_sender_chat, chat.bot, 'unban_chat_sender_chat') + assert check_defaults_handling(chat.unban_sender_chat, chat.bot) + + monkeypatch.setattr(chat.bot, 'unban_chat_sender_chat', make_assertion) + assert chat.unban_sender_chat(42) + + def test_unban_chat(self, monkeypatch, chat): + def make_assertion(*_, **kwargs): + chat_id = kwargs['chat_id'] == 42 + sender_chat_id = kwargs['sender_chat_id'] == chat.id + return chat_id and sender_chat_id + + assert check_shortcut_signature( + Chat.unban_chat, Bot.ban_chat_sender_chat, ['sender_chat_id'], [] + ) + assert check_shortcut_call(chat.unban_chat, chat.bot, 'unban_chat_sender_chat') + assert check_defaults_handling(chat.unban_chat, chat.bot) + + monkeypatch.setattr(chat.bot, 'unban_chat_sender_chat', make_assertion) + assert chat.unban_chat(42) + @pytest.mark.parametrize('is_anonymous', [True, False, None]) def test_promote_member(self, monkeypatch, chat, is_anonymous): def make_assertion(*_, **kwargs): diff --git a/tests/test_filters.py b/tests/test_filters.py index efebc477faf..364053f4a33 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1718,6 +1718,16 @@ def test_filters_sender_chat_channel(self, update): update.message.sender_chat = None assert not Filters.sender_chat.channel(update) + def test_filters_is_automatic_forward(self, update): + assert not Filters.is_automatic_forward(update) + update.message.is_automatic_forward = True + assert Filters.is_automatic_forward(update) + + def test_filters_has_protected_content(self, update): + assert not Filters.has_protected_content(update) + update.message.has_protected_content = True + assert Filters.has_protected_content(update) + def test_filters_invoice(self, update): assert not Filters.invoice(update) update.message.invoice = 'test' diff --git a/tests/test_message.py b/tests/test_message.py index 5ed66b4dcb7..1810502ac9f 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -180,6 +180,8 @@ def message(bot): ) }, {'sender_chat': Chat(-123, 'discussion_channel')}, + {'is_automatic_forward': True}, + {'has_protected_content': True}, ], ids=[ 'forwarded_user', @@ -229,6 +231,8 @@ def message(bot): 'voice_chat_ended', 'voice_chat_participants_invited', 'sender_chat', + 'is_automatic_forward', + 'has_protected_content', ], ) def message_params(bot, request): diff --git a/tests/test_user.py b/tests/test_user.py index dc801957158..14f4f50eec8 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. import pytest -from telegram import Update, User, Bot +from telegram import Update, User, Bot, InlineKeyboardButton from telegram.utils.helpers import escape_markdown from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling @@ -473,6 +473,13 @@ def test_mention_html(self, user): ) assert user.mention_html(user.username) == expected.format(user.id, user.username) + def test_mention_button(self, user): + expected_name = InlineKeyboardButton(text="Bob", url=f"tg://user?id={user.id}") + expected_full = InlineKeyboardButton(text=user.full_name, url=f"tg://user?id={user.id}") + + assert user.mention_button("Bob") == expected_name + assert user.mention_button() == expected_full + def test_mention_markdown(self, user): expected = '[{}](tg://user?id={})'