From 5ab449cd706bc8b9a804f77bcfc3992b55aca75e Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sat, 9 May 2026 14:46:34 +0300 Subject: [PATCH 1/4] Bump Bot API Version --- README.rst | 4 ++-- src/telegram/constants.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 84f80c9bb4f..44cbe11f12e 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-9.6-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-10.0-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -77,7 +77,7 @@ After installing_ the library, be sure to check out the section on `working with Telegram API support ~~~~~~~~~~~~~~~~~~~~ -All types and methods of the Telegram Bot API **9.6** are natively supported by this library. +All types and methods of the Telegram Bot API **10.0** are natively supported by this library. In addition, Bot API functionality not yet natively included can still be used as described `in our wiki `_. Notable Features diff --git a/src/telegram/constants.py b/src/telegram/constants.py index 144e6763892..fb0be0b48d2 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -181,7 +181,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=6) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=10, minor=0) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. From d671f3aff77b45f68ddd794e371387cb7fda803d Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sat, 9 May 2026 12:00:01 +0000 Subject: [PATCH 2/4] Add chango fragment for PR #5229 --- changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml diff --git a/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml b/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml new file mode 100644 index 00000000000..560895c4486 --- /dev/null +++ b/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml @@ -0,0 +1,5 @@ +features = "Full Support for Bot API 10.0" +[[pull_requests]] +uid = "5229" +author_uids = ["aelkheir"] +closes_threads = [] From 1b1dd487007cd5d5a2ef37abab56c3547570828b Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Wed, 20 May 2026 15:36:59 -0400 Subject: [PATCH 3/4] Bot API 10.0: `Chat Management` (#5230) --- .../5229.87PBN4GFkuAaDhhgFwCYkY.toml | 11 +- docs/source/inclusions/bot_methods.rst | 4 + src/telegram/_bot.py | 116 +++++++++++++++++- src/telegram/_chat.py | 78 ++++++++++++ src/telegram/_chatmember.py | 17 +++ src/telegram/_chatpermissions.py | 17 ++- src/telegram/_message.py | 39 ++++++ src/telegram/_user.py | 82 ++++++++++++- src/telegram/ext/_extbot.py | 54 ++++++++ tests/test_bot.py | 26 +++- tests/test_chat.py | 62 ++++++++++ tests/test_chatmember.py | 18 +++ tests/test_chatpermissions.py | 7 ++ tests/test_message.py | 26 ++++ tests/test_user.py | 45 +++++++ 15 files changed, 590 insertions(+), 12 deletions(-) diff --git a/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml b/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml index 560895c4486..7bb836c47c6 100644 --- a/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml +++ b/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml @@ -1,5 +1,6 @@ -features = "Full Support for Bot API 10.0" -[[pull_requests]] -uid = "5229" -author_uids = ["aelkheir"] -closes_threads = [] +highlights = "Full Support for Bot API 10.0" + +pull_requests = [ + { uid = "5229", author_uid = "aelkheir", closes_threads = ["5228"] }, + { uid = "5230", author_uid = "harshil21" }, +] diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 72f58dea7bd..8ee2734a81f 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -104,6 +104,10 @@ - Used for stopping the running poll * - :meth:`~telegram.Bot.set_message_reaction` - Used for setting reactions on messages + * - :meth:`~telegram.Bot.delete_message_reaction` + - Used for deleting reactions on messages + * - :meth:`~telegram.Bot.delete_all_message_reactions` + - Used for deleting all reactions by a chat or user .. raw:: html diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index d9d1f83b069..3491a75cffa 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -5125,6 +5125,7 @@ async def get_chat( async def get_chat_administrators( self, chat_id: str | int, + return_bots: bool | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -5140,18 +5141,21 @@ async def get_chat_administrators( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + return_bots (:obj:`bool`, optional): Pass :obj:`True` to additionally receive all bots + that are administrators of the chat. By default, bots other than the current bot + are omitted. + + .. versionadded:: NEXT.VERSION Returns: tuple[:class:`telegram.ChatMember`]: On success, returns a tuple of ``ChatMember`` - objects that contains information about all chat administrators except - other bots. If the chat is a group or a supergroup and no administrators were - appointed, only the creator will be returned. + objects that contains information about all chat administrators. Raises: :class:`telegram.error.TelegramError` """ - data: JSONDict = {"chat_id": chat_id} + data: JSONDict = {"chat_id": chat_id, "return_bots": return_bots} result = await self._post( "getChatAdministrators", data, @@ -12283,6 +12287,106 @@ async def save_prepared_keyboard_button( self, ) + async def delete_message_reaction( + self, + chat_id: int | str, + message_id: int, + user_id: int | None = None, + actor_chat_id: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """ + Use this method to remove a reaction from a message in a group or a supergroup chat. + The bot must have the :attr:`~telegram.ChatMemberAdministrator.can_delete_messages` + administrator right in the chat. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_id (:obj:`int`): Identifier of the target message. + user_id (:obj:`int`, optional): Identifier of the user whose reaction will be removed, + if the reaction were added by a user. + actor_chat_id (:obj:`int`, optional): Identifier of the chat whose reaction will be + removed, if the reaction were added by a chat. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "user_id": user_id, + "actor_chat_id": actor_chat_id, + } + + return await self._post( + "deleteMessageReaction", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_all_message_reactions( + self, + chat_id: int | str, + user_id: int | None = None, + actor_chat_id: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """ + Use this method to remove up to ``10000`` recent reactions in a group or a supergroup chat + added by a given user or chat. The bot must have the + :attr:`~telegram.ChatMemberAdministrator.can_delete_messages` administrator right in the + chat. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + user_id (:obj:`int`, optional): Identifier of the user whose reactions will be removed, + if the reactions were added by a user. + actor_chat_id (:obj:`int`, optional): Identifier of the chat whose reactions will be + removed, if the reactions were added by a chat. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "user_id": user_id, + "actor_chat_id": actor_chat_id, + } + + return await self._post( + "deleteAllMessageReactions", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -12629,3 +12733,7 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`replace_managed_bot_token`""" savePreparedKeyboardButton = save_prepared_keyboard_button """Alias for :meth:`save_prepared_keyboard_button`""" + deleteMessageReaction = delete_message_reaction + """Alias for :meth:`delete_message_reaction`""" + deleteAllMessageReactions = delete_all_message_reactions + """Alias for :meth:`delete_all_message_reactions`""" diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index 764da868e7e..c0164f36270 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -311,6 +311,7 @@ async def leave( async def get_administrators( self, + return_bots: bool | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -334,6 +335,7 @@ async def get_administrators( """ return await self.get_bot().get_chat_administrators( chat_id=self.id, + return_bots=return_bots, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -4049,6 +4051,82 @@ async def set_chat_member_tag( api_kwargs=api_kwargs, ) + async def delete_reaction( + self, + message_id: int, + user_id: int | None = None, + actor_chat_id: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """ + Shortcut for:: + + await bot.delete_message_reaction(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_message_reaction`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_message_reaction( + chat_id=self.id, + message_id=message_id, + user_id=user_id, + actor_chat_id=actor_chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_all_reactions( + self, + user_id: int | None = None, + actor_chat_id: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """ + Shortcut for:: + + await bot.delete_all_message_reactions( + chat_id=update.effective_chat.id, + *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_all_message_reactions`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_all_message_reactions( + chat_id=self.id, + user_id=user_id, + actor_chat_id=actor_chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + class Chat(_ChatBase): """This object represents a chat. diff --git a/src/telegram/_chatmember.py b/src/telegram/_chatmember.py index be55c8469e8..8ad48616376 100644 --- a/src/telegram/_chatmember.py +++ b/src/telegram/_chatmember.py @@ -517,6 +517,10 @@ class ChatMemberRestricted(ChatMember): can_edit_tag (:obj:`bool`): :obj:`True`, if the user is allowed to edit their own tag. .. versionadded:: 22.7 + can_react_to_messages (:obj:`bool`): :obj:`True`, if the user is allowed to react to + messages. + + .. versionadded:: NEXT.VERSION tag (:obj:`str`, optional): Tag of the member. .. versionadded:: 22.7 @@ -573,6 +577,10 @@ class ChatMemberRestricted(ChatMember): can_edit_tag (:obj:`bool`): :obj:`True`, if the user is allowed to edit their own tag. .. versionadded:: 22.7 + can_react_to_messages (:obj:`bool`): :obj:`True`, if the user is allowed to react to + messages. + + .. versionadded:: NEXT.VERSION tag (:obj:`str`): Optional. Tag of the member. .. versionadded:: 22.7 @@ -586,6 +594,7 @@ class ChatMemberRestricted(ChatMember): "can_invite_users", "can_manage_topics", "can_pin_messages", + "can_react_to_messages", "can_send_audios", "can_send_documents", "can_send_messages", @@ -621,10 +630,17 @@ def __init__( can_send_voice_notes: bool, can_edit_tag: bool, tag: str | None = None, + # tags: NEXT.VERSION + # temporarily optional to make it not breaking + can_react_to_messages: bool | None = None, *, api_kwargs: JSONDict | None = None, ): super().__init__(status=ChatMember.RESTRICTED, user=user, api_kwargs=api_kwargs) + + if can_react_to_messages is None: + raise TypeError("`can_react_to_messages` is required and cannot be None") + with self._unfrozen(): self.is_member: bool = is_member self.can_change_info: bool = can_change_info @@ -643,6 +659,7 @@ def __init__( self.can_send_video_notes: bool = can_send_video_notes self.can_send_voice_notes: bool = can_send_voice_notes self.can_edit_tag: bool = can_edit_tag + self.can_react_to_messages: bool = can_react_to_messages self.tag: str | None = tag diff --git a/src/telegram/_chatpermissions.py b/src/telegram/_chatpermissions.py index 2116f4f4c33..7ca1871473d 100644 --- a/src/telegram/_chatpermissions.py +++ b/src/telegram/_chatpermissions.py @@ -36,7 +36,7 @@ class ChatPermissions(TelegramObject): :attr:`can_change_info`, :attr:`can_invite_users`, :attr:`can_pin_messages`, :attr:`can_send_audios`, :attr:`can_send_documents`, :attr:`can_send_photos`, :attr:`can_send_videos`, :attr:`can_send_video_notes`, :attr:`can_send_voice_notes`, - :attr:`can_manage_topics` and :attr:`can_edit_tag` are equal. + :attr:`can_manage_topics`, :attr:`can_edit_tag`, and :attr:`can_react_to_messages` are equal. .. versionchanged:: 20.0 :attr:`can_manage_topics` is considered as well when comparing objects of @@ -50,6 +50,9 @@ class ChatPermissions(TelegramObject): .. versionchanged:: 22.7 :attr:`can_edit_tag` is considered as well when comparing objects of this type in terms of equality. + .. versionchanged:: NEXT.VERSION + :attr:`can_react_to_messages` is considered as well when comparing objects of + this type in terms of equality. Note: @@ -100,6 +103,10 @@ class ChatPermissions(TelegramObject): tag. .. versionadded:: 22.7 + can_react_to_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to react + to messages. If omitted, defaults to the value of :attr:`can_send_messages`. + + .. versionadded:: NEXT.VERSION Attributes: can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text @@ -145,6 +152,10 @@ class ChatPermissions(TelegramObject): tag. .. versionadded:: 22.7 + can_react_to_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to react + to messages. If omitted, defaults to the value of :attr:`can_send_messages`. + + .. versionadded:: NEXT.VERSION """ @@ -155,6 +166,7 @@ class ChatPermissions(TelegramObject): "can_invite_users", "can_manage_topics", "can_pin_messages", + "can_react_to_messages", "can_send_audios", "can_send_documents", "can_send_messages", @@ -183,6 +195,7 @@ def __init__( can_send_video_notes: bool | None = None, can_send_voice_notes: bool | None = None, can_edit_tag: bool | None = None, + can_react_to_messages: bool | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -203,6 +216,7 @@ def __init__( self.can_send_video_notes: bool | None = can_send_video_notes self.can_send_voice_notes: bool | None = can_send_voice_notes self.can_edit_tag: bool | None = can_edit_tag + self.can_react_to_messages: bool | None = can_react_to_messages self._id_attrs = ( self.can_send_messages, @@ -220,6 +234,7 @@ def __init__( self.can_send_video_notes, self.can_send_voice_notes, self.can_edit_tag, + self.can_react_to_messages, ) self._freeze() diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 8c6626f50a6..b87b8bdae4b 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -5220,6 +5220,45 @@ async def decline_suggested_post( api_kwargs=api_kwargs, ) + async def delete_reaction( + self, + user_id: int | None = None, + actor_chat_id: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """Shortcut for:: + + await bot.delete_message_reaction( + chat_id=message.chat_id, + message_id=message.message_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_message_reaction`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_message_reaction( + chat_id=self.chat_id, + message_id=self.message_id, + user_id=user_id, + actor_chat_id=actor_chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def parse_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. diff --git a/src/telegram/_user.py b/src/telegram/_user.py index 821f230adb6..a2b53d6fede 100644 --- a/src/telegram/_user.py +++ b/src/telegram/_user.py @@ -2761,7 +2761,7 @@ async def replace_token( .. versionadded:: NEXT.VERSION Returns: - :obj:`bool`: On success, :obj:`str` is returned. + :obj:`str`: On success, :obj:`str` is returned. """ return await self.get_bot().replace_managed_bot_token( user_id=self.id, @@ -2771,3 +2771,83 @@ async def replace_token( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) + + async def delete_reaction( + self, + chat_id: int | str, + message_id: int, + actor_chat_id: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """ + Shortcut for:: + + await bot.delete_message_reaction( + user_id=update.effective_user.id, + *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_message_reaction`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_message_reaction( + user_id=self.id, + chat_id=chat_id, + message_id=message_id, + actor_chat_id=actor_chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_all_reactions( + self, + chat_id: int | str, + actor_chat_id: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """ + Shortcut for:: + + await bot.delete_all_message_reactions( + user_id=update.effective_user.id, + *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_all_message_reactions`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_all_message_reactions( + chat_id=chat_id, + user_id=self.id, + actor_chat_id=actor_chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index 931115f7beb..b6595fe0e98 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -1844,6 +1844,7 @@ async def forward_messages( async def get_chat_administrators( self, chat_id: str | int, + return_bots: bool | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1854,6 +1855,7 @@ async def get_chat_administrators( ) -> tuple[ChatMember, ...]: return await super().get_chat_administrators( chat_id=chat_id, + return_bots=return_bots, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -5593,6 +5595,56 @@ async def save_prepared_keyboard_button( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def delete_message_reaction( + self, + chat_id: int | str, + message_id: int, + user_id: int | None = None, + actor_chat_id: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> bool: + return await super().delete_message_reaction( + chat_id=chat_id, + message_id=message_id, + user_id=user_id, + actor_chat_id=actor_chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def delete_all_message_reactions( + self, + chat_id: int | str, + user_id: int | None = None, + actor_chat_id: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> bool: + return await super().delete_all_message_reactions( + chat_id=chat_id, + user_id=user_id, + actor_chat_id=actor_chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + # updated camelCase aliases getMe = get_me sendMessage = send_message @@ -5762,3 +5814,5 @@ async def save_prepared_keyboard_button( getManagedBotToken = get_managed_bot_token replaceManagedBotToken = replace_managed_bot_token savePreparedKeyboardButton = save_prepared_keyboard_button + deleteMessageReaction = delete_message_reaction + deleteAllMessageReactions = delete_all_message_reactions diff --git a/tests/test_bot.py b/tests/test_bot.py index 2fd77550ecb..15673347538 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2920,6 +2920,26 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): ) assert isinstance(inst, PreparedKeyboardButton) + # Bots cannot delete their own reaction from my testing, so we aren't making a real request + async def test_delete_message_reaction(self, offline_bot, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("chat_id") == 1234 + assert request_data.parameters.get("message_id") == 12 + assert request_data.parameters.get("user_id") == 3432 + assert request_data.parameters.get("actor_chat_id") == 1232 + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + await offline_bot.delete_message_reaction(1234, 12, 3432, 1232) + + async def test_delete_all_message_reactions(self, offline_bot, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("chat_id") == 1234 + assert request_data.parameters.get("user_id") == 3432 + assert request_data.parameters.get("actor_chat_id") == 1232 + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + await offline_bot.delete_all_message_reactions(1234, 3432, 1232) + class TestBotWithRequest: """ @@ -3705,11 +3725,15 @@ async def test_get_chat(self, bot, super_group_id): assert cfi.id == int(super_group_id) async def test_get_chat_administrators(self, bot, channel_id): - admins = await bot.get_chat_administrators(channel_id) + admins = await bot.get_chat_administrators(channel_id, return_bots=True) assert isinstance(admins, tuple) + bots_found = 0 for a in admins: assert a.status in ("administrator", "creator") + if a.user.is_bot: + bots_found += 1 + assert bots_found > 1 # will be False if return_bots=False async def test_get_chat_member_count(self, bot, channel_id): count = await bot.get_chat_member_count(channel_id) diff --git a/tests/test_chat.py b/tests/test_chat.py index 8d1d2db23ce..b6159896200 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1537,6 +1537,68 @@ async def make_assertion(*_, **kwargs): active_period=3600, ) + async def test_instance_method_delete_reaction(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["message_id"] == 321 + and kwargs["user_id"] == 123 + and kwargs["actor_chat_id"] == 222 + ) + + assert check_shortcut_signature( + Chat.delete_reaction, + Bot.delete_message_reaction, + [ + "chat_id", + ], + additional_kwargs=[], + ) + assert await check_shortcut_call( + chat.delete_reaction, + chat.get_bot(), + "delete_message_reaction", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.delete_reaction, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "delete_message_reaction", make_assertion) + assert await chat.delete_reaction( + user_id=123, + message_id=321, + actor_chat_id=222, + ) + + async def test_instance_method_delete_all_reactions(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["user_id"] == 123 + and kwargs["actor_chat_id"] == 222 + ) + + assert check_shortcut_signature( + Chat.delete_all_reactions, + Bot.delete_all_message_reactions, + [ + "chat_id", + ], + additional_kwargs=[], + ) + assert await check_shortcut_call( + chat.delete_all_reactions, + chat.get_bot(), + "delete_all_message_reactions", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.delete_all_reactions, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "delete_all_message_reactions", make_assertion) + assert await chat.delete_all_reactions( + user_id=123, + actor_chat_id=222, + ) + async def test_instance_method_get_gifts(self, monkeypatch, chat): async def make_assertion(*_, **kwargs): return kwargs["chat_id"] == chat.id diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index 7bc4a2979f6..ead273bdbd2 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -61,6 +61,7 @@ class ChatMemberTestBase: can_pin_messages = True can_post_stories = True can_edit_stories = True + can_react_to_messages = True can_delete_stories = True can_manage_topics = True until_date = dtm.datetime.now(UTC).replace(microsecond=0) @@ -576,6 +577,7 @@ def chat_member_restricted(): is_member=TestChatMemberRestrictedWithoutRequest.is_member, until_date=TestChatMemberRestrictedWithoutRequest.until_date, can_edit_tag=TestChatMemberRestrictedWithoutRequest.can_edit_tag, + can_react_to_messages=TestChatMemberRestrictedWithoutRequest.can_react_to_messages, tag=TestChatMemberRestrictedWithoutRequest.tag, ) @@ -609,6 +611,7 @@ def test_de_json(self, offline_bot): "is_member": self.is_member, "until_date": to_timestamp(self.until_date), "can_edit_tag": self.can_edit_tag, + "can_react_to_messages": self.can_react_to_messages, "tag": self.tag, # legacy argument "can_send_media_messages": False, @@ -636,6 +639,7 @@ def test_de_json(self, offline_bot): assert chat_member.is_member == self.is_member assert chat_member.until_date == self.until_date assert chat_member.can_edit_tag == self.can_edit_tag + assert chat_member.can_react_to_messages == self.can_react_to_messages assert chat_member.tag == self.tag def test_de_json_localization(self, tz_bot, offline_bot, raw_bot, chat_member_restricted): @@ -676,9 +680,22 @@ def test_to_dict(self, chat_member_restricted): "is_member": chat_member_restricted.is_member, "until_date": to_timestamp(chat_member_restricted.until_date), "can_edit_tag": chat_member_restricted.can_edit_tag, + "can_react_to_messages": chat_member_restricted.can_react_to_messages, "tag": chat_member_restricted.tag, } + def test_can_react_to_messages_raises(self, chat_member_restricted): + with pytest.raises( + TypeError, match="`can_react_to_messages` is required and cannot be None" + ): + ChatMemberRestricted( + *[ + getattr(chat_member_restricted, k) + for k in chat_member_restricted.__slots__ + if k != "can_react_to_messages" + ] + ) + def test_equality(self, chat_member_restricted): a = chat_member_restricted b = deepcopy(chat_member_restricted) @@ -701,6 +718,7 @@ def test_equality(self, chat_member_restricted): False, False, False, + False, "tag", ) d = Dice(5, "test") diff --git a/tests/test_chatpermissions.py b/tests/test_chatpermissions.py index 0baee3f89d7..43aeabcf537 100644 --- a/tests/test_chatpermissions.py +++ b/tests/test_chatpermissions.py @@ -41,6 +41,7 @@ def chat_permissions(): can_send_video_notes=True, can_send_voice_notes=True, can_edit_tag=True, + can_react_to_messages=True, ) @@ -60,6 +61,7 @@ class ChatPermissionsTestBase: can_send_video_notes = False can_send_voice_notes = None can_edit_tag = None + can_react_to_messages = True class TestChatPermissionsWithoutRequest(ChatPermissionsTestBase): @@ -86,6 +88,7 @@ def test_de_json(self, offline_bot): "can_send_video_notes": self.can_send_video_notes, "can_send_voice_notes": self.can_send_voice_notes, "can_edit_tag": self.can_edit_tag, + "can_react_to_messages": self.can_react_to_messages, } permissions = ChatPermissions.de_json(json_dict, offline_bot) assert permissions.api_kwargs == {"can_send_media_messages": "can_send_media_messages"} @@ -105,6 +108,7 @@ def test_de_json(self, offline_bot): assert permissions.can_send_video_notes == self.can_send_video_notes assert permissions.can_send_voice_notes == self.can_send_voice_notes assert permissions.can_edit_tag == self.can_edit_tag + assert permissions.can_react_to_messages == self.can_react_to_messages def test_to_dict(self, chat_permissions): permissions_dict = chat_permissions.to_dict() @@ -130,6 +134,7 @@ def test_to_dict(self, chat_permissions): assert permissions_dict["can_send_video_notes"] == chat_permissions.can_send_video_notes assert permissions_dict["can_send_voice_notes"] == chat_permissions.can_send_voice_notes assert permissions_dict["can_edit_tag"] == chat_permissions.can_edit_tag + assert permissions_dict["can_react_to_messages"] == chat_permissions.can_react_to_messages def test_equality(self): a = ChatPermissions( @@ -159,6 +164,7 @@ def test_equality(self): can_send_video_notes=True, can_send_voice_notes=True, can_edit_tag=True, + can_react_to_messages=True, ) f = ChatPermissions( can_send_messages=True, @@ -171,6 +177,7 @@ def test_equality(self): can_send_video_notes=True, can_send_voice_notes=True, can_edit_tag=True, + can_react_to_messages=True, ) assert a == b diff --git a/tests/test_message.py b/tests/test_message.py index 958c59b3109..700491f2752 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -3419,3 +3419,29 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(message.get_bot(), "decline_suggested_post", make_assertion) assert await message.decline_suggested_post(comment="some comment") + + async def test_delete_reaction(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_id"] == message.message_id + and kwargs["user_id"] == 23 + and kwargs["actor_chat_id"] == 12 + ) + + assert check_shortcut_signature( + Message.delete_reaction, + Bot.delete_message_reaction, + ["chat_id", "message_id"], + [], + ) + assert await check_shortcut_call( + message.delete_reaction, + message.get_bot(), + "delete_message_reaction", + shortcut_kwargs=["chat_id", "message_id"], + ) + assert await check_defaults_handling(message.delete_reaction, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "delete_message_reaction", make_assertion) + assert await message.delete_reaction(user_id=23, actor_chat_id=12) diff --git a/tests/test_user.py b/tests/test_user.py index 24f8ffe8c8d..4d6d35755e5 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -926,3 +926,48 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(user.get_bot(), "replace_managed_bot_token", make_assertion) assert await user.replace_token() + + async def test_instance_method_delete_reaction(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return ( + kwargs["user_id"] == user.id + and kwargs["chat_id"] == 1234 + and kwargs["message_id"] == 123 + and kwargs["actor_chat_id"] == 42 + ) + + assert check_shortcut_signature( + user.delete_reaction, Bot.delete_message_reaction, ["user_id"], [] + ) + assert await check_shortcut_call( + user.delete_reaction, + user.get_bot(), + "delete_message_reaction", + shortcut_kwargs=["user_id"], + ) + assert await check_defaults_handling(user.delete_reaction, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "delete_message_reaction", make_assertion) + assert await user.delete_reaction(chat_id=1234, message_id=123, actor_chat_id=42) + + async def test_instance_method_delete_all_reactions(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return ( + kwargs["user_id"] == user.id + and kwargs["chat_id"] == 1234 + and kwargs["actor_chat_id"] == 42 + ) + + assert check_shortcut_signature( + user.delete_all_reactions, Bot.delete_all_message_reactions, ["user_id"], [] + ) + assert await check_shortcut_call( + user.delete_all_reactions, + user.get_bot(), + "delete_all_message_reactions", + shortcut_kwargs=["user_id"], + ) + assert await check_defaults_handling(user.delete_all_reactions, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "delete_all_message_reactions", make_assertion) + assert await user.delete_all_reactions(chat_id=1234, actor_chat_id=42) From 2ac87371bd824fd8206e5be69815431250ab7618 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 26 May 2026 09:28:49 -0400 Subject: [PATCH 4/4] Bot API 10.0: General (#5238) --- .../5229.87PBN4GFkuAaDhhgFwCYkY.toml | 2 + docs/source/inclusions/bot_methods.rst | 6 + docs/source/telegram.at-tree.rst | 1 + docs/source/telegram.botaccesssettings.rst | 6 + src/telegram/__init__.py | 2 + src/telegram/_bot.py | 170 ++++++++++++++++-- src/telegram/_botaccesssettings.py | 77 ++++++++ src/telegram/_chat.py | 5 +- src/telegram/_message.py | 5 +- src/telegram/_user.py | 119 +++++++++++- src/telegram/constants.py | 40 +++++ src/telegram/ext/_extbot.py | 74 +++++++- tests/auxil/dummy_objects.py | 2 + tests/test_bot.py | 43 +++++ tests/test_botaccesssettings.py | 87 +++++++++ tests/test_user.py | 67 +++++++ 16 files changed, 692 insertions(+), 14 deletions(-) create mode 100644 docs/source/telegram.botaccesssettings.rst create mode 100644 src/telegram/_botaccesssettings.py create mode 100644 tests/test_botaccesssettings.py diff --git a/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml b/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml index 7bb836c47c6..04754f99219 100644 --- a/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml +++ b/changes/unreleased/5229.87PBN4GFkuAaDhhgFwCYkY.toml @@ -3,4 +3,6 @@ highlights = "Full Support for Bot API 10.0" pull_requests = [ { uid = "5229", author_uid = "aelkheir", closes_threads = ["5228"] }, { uid = "5230", author_uid = "harshil21" }, + { uid = "5235", author_uid = "harshil21" }, + { uid = "5238", author_uid = "harshil21" }, ] diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 8ee2734a81f..3b9fdfd626c 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -171,6 +171,8 @@ - Used for unpinning a message * - :meth:`~telegram.Bot.unpin_all_chat_messages` - Used for unpinning all pinned chat messages + * - :meth:`~telegram.Bot.get_user_personal_chat_messages` + - Used for obtaining the personal chat messages of a user * - :meth:`~telegram.Bot.get_user_profile_audios` - Used for obtaining user's profile audios * - :meth:`~telegram.Bot.get_user_profile_photos` @@ -241,6 +243,10 @@ - Used for obtaining the menu button of a private chat or the default menu button * - :meth:`~telegram.Bot.set_chat_menu_button` - Used for setting the menu button of a private chat or the default menu button + * - :meth:`~telegram.Bot.set_managed_bot_access_settings` + - Used for changing the access settings of a managed bot + * - :meth:`~telegram.Bot.get_managed_bot_access_settings` + - Used for obtaining the access settings of a managed bot * - :meth:`~telegram.Bot.set_my_description` - Used for setting the description of the bot * - :meth:`~telegram.Bot.get_my_description` diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 25d9c269838..716f7ab4361 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -8,6 +8,7 @@ Available Types telegram.animation telegram.audio telegram.birthdate + telegram.botaccesssettings telegram.botcommand telegram.botcommandscope telegram.botcommandscopeallchatadministrators diff --git a/docs/source/telegram.botaccesssettings.rst b/docs/source/telegram.botaccesssettings.rst new file mode 100644 index 00000000000..788689b9266 --- /dev/null +++ b/docs/source/telegram.botaccesssettings.rst @@ -0,0 +1,6 @@ +BotAccessSettings +================= + +.. autoclass:: telegram.BotAccessSettings + :members: + :show-inheritance: \ No newline at end of file diff --git a/src/telegram/__init__.py b/src/telegram/__init__.py index 362f3253198..db9aa78a7fb 100644 --- a/src/telegram/__init__.py +++ b/src/telegram/__init__.py @@ -35,6 +35,7 @@ "BackgroundTypeWallpaper", "Birthdate", "Bot", + "BotAccessSettings", "BotCommand", "BotCommandScope", "BotCommandScopeAllChatAdministrators", @@ -348,6 +349,7 @@ from . import _version, constants, error, helpers, request, warnings from ._birthdate import Birthdate from ._bot import Bot +from ._botaccesssettings import BotAccessSettings from ._botcommand import BotCommand from ._botcommandscope import ( BotCommandScope, diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 3491a75cffa..5493cb21709 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -47,7 +47,8 @@ serialization = None # type: ignore[assignment] CRYPTO_INSTALLED = False -from telegram._botcommand import BotCommand # pylint: disable=ungrouped-imports +from telegram._botaccesssettings import BotAccessSettings # pylint: disable=ungrouped-imports +from telegram._botcommand import BotCommand from telegram._botcommandscope import BotCommandScope from telegram._botdescription import BotDescription, BotShortDescription from telegram._botname import BotName @@ -1209,7 +1210,7 @@ async def send_message_draft( self, chat_id: int, draft_id: int, - text: str, + text: str | None = None, message_thread_id: int | None = None, parse_mode: ODVInput[str] = DEFAULT_NONE, entities: Sequence["MessageEntity"] | None = None, @@ -1221,7 +1222,9 @@ async def send_message_draft( api_kwargs: JSONDict | None = None, ) -> bool: """Use this method to stream a partial message to a user while the message is being - generated. + generated. Note that the streamed draft is ephemeral and acts as a temporary 30-second + preview - once the output is finalized, you must call :meth:`~Bot.send_message` with + the complete message to persist it in the user's chat. .. versionadded:: 22.6 @@ -1233,19 +1236,21 @@ async def send_message_draft( chat_id (:obj:`int`): Unique identifier for the target private chat. draft_id (:obj:`int`): Unique identifier of the message draft; must be non-zero. Changes of drafts with the same identifier are animated. - text (:obj:`str`): Text of the message to be sent, - :tg-const:`telegram.constants.MessageLimit.MIN_TEXT_LENGTH`- - :tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after - entities parsing. + text (:obj:`str`, optional): Text of the message to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after + entities parsing. Pass an empty text to show a "Thinking..." placeholder. + + .. versionchanged:: NEXT.VERSION + Bot API 10.0 now makes this an optional parameter. + + message_thread_id (:obj:`int`, optional): Unique identifier for the target + message thread. parse_mode (:obj:`str`): |parse_mode| entities (Sequence[:class:`telegram.MessageEntity`], optional): Sequence of special entities that appear in message text, which can be specified instead of :paramref:`parse_mode`. |sequenceargs| - message_thread_id (:obj:`int`, optional): Unique identifier for the target - message thread. - Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -11127,6 +11132,145 @@ async def edit_user_star_subscription( api_kwargs=api_kwargs, ) + async def get_managed_bot_access_settings( + self, + user_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> BotAccessSettings: + """ + Use this method to get the access settings of a managed bot. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): User identifier of the managed bot whose access settings will be + returned. + + Returns: + :class:`telegram.BotAccessSettings`: The access settings of the managed bot. + + Raises: + :class:`telegram.error.TelegramError` + """ + + data: JSONDict = { + "user_id": user_id, + } + + return BotAccessSettings.de_json( + await self._post( + "getManagedBotAccessSettings", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + self, + ) + + async def set_managed_bot_access_settings( + self, + user_id: int, + is_access_restricted: bool, + added_user_ids: Sequence[int] | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """ + Use this method to change the access settings of a managed bot. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): User identifier of the managed bot whose access settings will be + changed. + is_access_restricted (:obj:`bool`): Pass :obj:`True`, if only selected users can access + the bot. The bot's owner can always access it. + added_user_ids (Sequence[:obj:`int`], optional): A list of up to + :tg-const:`telegram.constants.ManagedBotAccessLimit.MAX_ALLOWED_USERS` + identifiers of users who will have access to the bot in addition to its owner. + Ignored if :paramref:`is_access_restricted` is :obj:`False`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + + data: JSONDict = { + "user_id": user_id, + "is_access_restricted": is_access_restricted, + "added_user_ids": added_user_ids, + } + + return await self._post( + "setManagedBotAccessSettings", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_user_personal_chat_messages( + self, + user_id: int, + limit: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> tuple[Message, ...]: + """ + Use this method to get the last messages from the personal chat (i.e., the chat currently + added to their profile) of a given user. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): Unique identifier of the target user. + limit (:obj:`int`): The maximum number of messages to return; + :tg-const:`telegram.constants.PersonalChatMessagesLimit.MIN_LIMIT`- + :tg-const:`telegram.constants.PersonalChatMessagesLimit.MAX_LIMIT`. + + Returns: + tuple[:class:`telegram.Message`, ...]: On success, a tuple of + :class:`telegram.Message` objects is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + + data: JSONDict = {"user_id": user_id, "limit": limit} + + return Message.de_list( + await self._post( + "getUserPersonalChatMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + self, + ) + async def send_paid_media( self, chat_id: str | int, @@ -12733,6 +12877,12 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`replace_managed_bot_token`""" savePreparedKeyboardButton = save_prepared_keyboard_button """Alias for :meth:`save_prepared_keyboard_button`""" + getManagedBotAccessSettings = get_managed_bot_access_settings + """Alias for :meth:`get_managed_bot_access_settings`""" + setManagedBotAccessSettings = set_managed_bot_access_settings + """Alias for :meth:`set_managed_bot_access_settings`""" + getUserPersonalChatMessages = get_user_personal_chat_messages + """Alias for :meth:`get_user_personal_chat_messages`""" deleteMessageReaction = delete_message_reaction """Alias for :meth:`delete_message_reaction`""" deleteAllMessageReactions = delete_all_message_reactions diff --git a/src/telegram/_botaccesssettings.py b/src/telegram/_botaccesssettings.py new file mode 100644 index 00000000000..745d63ae67b --- /dev/null +++ b/src/telegram/_botaccesssettings.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# 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 Telegram Bot Access Settings.""" + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class BotAccessSettings(TelegramObject): + """ + This object describes the access settings of a bot. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`is_access_restricted` and :attr:`added_users` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + is_access_restricted (:obj:`bool`): :obj:`True`, if only selected users can access the bot. + The bot's owner can always access it. + added_users (Sequence[:class:`telegram.User`], optional): The list of other users who + have access to the bot if the access is restricted. + + Attributes: + is_access_restricted (:obj:`bool`): :obj:`True`, if only selected users can access the bot. + The bot's owner can always access it. + added_users (Sequence[:class:`telegram.User`]): Optional. The list of other users who + have access to the bot if the access is restricted. + """ + + __slots__ = ("added_users", "is_access_restricted") + + def __init__( + self, + is_access_restricted: bool, + added_users: Sequence[User] | None = None, + *, + api_kwargs: JSONDict | None = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.is_access_restricted: bool = is_access_restricted + self.added_users: tuple[User, ...] = parse_sequence_arg(added_users) + + self._id_attrs = (self.is_access_restricted, self.added_users) + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BotAccessSettings": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + data["added_users"] = de_list_optional(data.get("added_users"), User, bot) + + return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index c0164f36270..f3842aaf4ec 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -1090,7 +1090,7 @@ async def send_message( async def send_message_draft( self, draft_id: int, - text: str, + text: str | None = None, message_thread_id: int | None = None, parse_mode: ODVInput[str] = DEFAULT_NONE, entities: Sequence["MessageEntity"] | None = None, @@ -1107,6 +1107,9 @@ async def send_message_draft( For the documentation of the arguments, please see :meth:`telegram.Bot.send_message_draft`. + .. versionchanged:: NEXT.VERSION + Bot API 10.0 makes the ``text`` argument optional. + Returns: :obj:`bool`: On success, :obj:`True` is returned. diff --git a/src/telegram/_message.py b/src/telegram/_message.py index b87b8bdae4b..9ac9ee3eeb2 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -2186,7 +2186,7 @@ async def reply_text( async def reply_text_draft( self, draft_id: int, - text: str, + text: str | None = None, parse_mode: ODVInput[str] = DEFAULT_NONE, entities: Sequence["MessageEntity"] | None = None, message_thread_id: ODVInput[int] = DEFAULT_NONE, @@ -2213,6 +2213,9 @@ async def reply_text_draft( .. versionadded:: 22.6 + .. versionchanged:: NEXT.VERSION + Bot API 10.0 makes the ``text`` argument optional. + Returns: :obj:`bool`: On success, :obj:`True` is returned. diff --git a/src/telegram/_user.py b/src/telegram/_user.py index a2b53d6fede..2bf6c8af2e9 100644 --- a/src/telegram/_user.py +++ b/src/telegram/_user.py @@ -42,6 +42,7 @@ from telegram import ( Animation, Audio, + BotAccessSettings, Contact, Document, Gift, @@ -528,7 +529,7 @@ async def send_message( async def send_message_draft( self, draft_id: int, - text: str, + text: str | None = None, message_thread_id: int | None = None, parse_mode: ODVInput[str] = DEFAULT_NONE, entities: Sequence["MessageEntity"] | None = None, @@ -550,6 +551,9 @@ async def send_message_draft( .. versionadded:: 22.6 + .. versionchanged:: NEXT.VERSION + Bot API 10.0 makes the ``text`` argument optional. + Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -2772,6 +2776,81 @@ async def replace_token( api_kwargs=api_kwargs, ) + async def get_managed_bot_access_settings( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> "BotAccessSettings": + """ + Shortcut for:: + + await bot.get_managed_bot_access_settings( + user_id=update.effective_user.id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_managed_bot_access_settings`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.BotAccessSettings`: On success, returns the access settings of the bot + managed by the user. + """ + + return await self.get_bot().get_managed_bot_access_settings( + user_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_managed_bot_access_settings( + self, + is_access_restricted: bool, + added_user_ids: Sequence[int] | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """ + Shortcut for:: + + await bot.set_managed_bot_access_settings( + user_id=update.effective_user.id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_managed_bot_access_settings`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + + return await self.get_bot().set_managed_bot_access_settings( + user_id=self.id, + is_access_restricted=is_access_restricted, + added_user_ids=added_user_ids, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def delete_reaction( self, chat_id: int | str, @@ -2813,6 +2892,44 @@ async def delete_reaction( api_kwargs=api_kwargs, ) + async def get_personal_chat_messages( + self, + limit: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> tuple["Message", ...]: + """ + Shortcut for:: + + await bot.get_user_personal_chat_messages( + user_id=update.effective_user.id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_user_personal_chat_messages`. + + .. versionadded:: NEXT.VERSION + + Returns: + tuple[:class:`telegram.Message`]: On success, a tuple of messages from the personal + channel chat is returned. + """ + + return await self.get_bot().get_user_personal_chat_messages( + user_id=self.id, + limit=limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def delete_all_reactions( self, chat_id: int | str, diff --git a/src/telegram/constants.py b/src/telegram/constants.py index fb0be0b48d2..a71ade00d3d 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -87,6 +87,7 @@ "KeyboardButtonRequestUsersLimit", "KeyboardButtonStyle", "LocationLimit", + "ManagedBotAccessLimit", "MaskPosition", "MediaGroupLimit", "MenuButtonType", @@ -101,6 +102,7 @@ "OwnedGiftType", "PaidMediaType", "ParseMode", + "PersonalChatMessagesLimit", "PollLimit", "PollType", "PollingLimit", @@ -2568,6 +2570,28 @@ class PaidMediaType(StringEnum): """:obj:`str`: The type of :class:`telegram.PaidMediaPhoto`.""" +class PersonalChatMessagesLimit(IntEnum): + """This enum contains limitations for + :paramref:`telegram.Bot.get_user_personal_chat_messages.limit`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MIN_LIMIT = 1 + """:obj:`int`: Minimum value allowed for the + :paramref:`~telegram.Bot.get_user_personal_chat_messages.limit` + parameter of :meth:`telegram.Bot.get_user_personal_chat_messages`. + """ + MAX_LIMIT = 20 + """:obj:`int`: Maximum value allowed for the + :paramref:`~telegram.Bot.get_user_personal_chat_messages.limit` + parameter of :meth:`telegram.Bot.get_user_personal_chat_messages`. + """ + + class PollingLimit(IntEnum): """This enum contains limitations for :paramref:`telegram.Bot.get_updates.limit`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. @@ -4039,6 +4063,22 @@ class ReactionEmoji(StringEnum): """:obj:`str`: Pouting face""" +class ManagedBotAccessLimit(IntEnum): + """This enum contains limitations for :meth:`~telegram.Bot.set_managed_bot_access_settings`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MAX_ALLOWED_USERS = 10 + """:obj:`int`: Maximum number of users that can be allowed to access a managed bot in the + :paramref:`~telegram.Bot.set_managed_bot_access_settings.added_user_ids` parameter of + :meth:`~telegram.Bot.set_managed_bot_access_settings`. + """ + + class VerifyLimit(IntEnum): """This enum contains limitations for :meth:`~telegram.Bot.verify_chat` and :meth:`~telegram.Bot.verify_user`. diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index b6595fe0e98..7b468dade61 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -38,6 +38,7 @@ Animation, Audio, Bot, + BotAccessSettings, BotCommand, BotCommandScope, BotDescription, @@ -3150,7 +3151,7 @@ async def send_message_draft( self, chat_id: int, draft_id: int, - text: str, + text: str | None = None, message_thread_id: int | None = None, parse_mode: ODVInput[str] = DEFAULT_NONE, entities: Sequence["MessageEntity"] | None = None, @@ -5034,6 +5035,74 @@ async def edit_user_star_subscription( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_managed_bot_access_settings( + self, + user_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> BotAccessSettings: + + return await super().get_managed_bot_access_settings( + user_id=user_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def set_managed_bot_access_settings( + self, + user_id: int, + is_access_restricted: bool, + added_user_ids: Sequence[int] | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> bool: + + return await super().set_managed_bot_access_settings( + user_id=user_id, + is_access_restricted=is_access_restricted, + added_user_ids=added_user_ids, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def get_user_personal_chat_messages( + self, + user_id: int, + limit: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> tuple[Message, ...]: + return await super().get_user_personal_chat_messages( + user_id=user_id, + limit=limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def send_paid_media( self, chat_id: str | int, @@ -5814,5 +5883,8 @@ async def delete_all_message_reactions( getManagedBotToken = get_managed_bot_token replaceManagedBotToken = replace_managed_bot_token savePreparedKeyboardButton = save_prepared_keyboard_button + getManagedBotAccessSettings = get_managed_bot_access_settings + setManagedBotAccessSettings = set_managed_bot_access_settings + getUserPersonalChatMessages = get_user_personal_chat_messages deleteMessageReaction = delete_message_reaction deleteAllMessageReactions = delete_all_message_reactions diff --git a/tests/auxil/dummy_objects.py b/tests/auxil/dummy_objects.py index c0ff0ce0cf5..4a0b0748fb6 100644 --- a/tests/auxil/dummy_objects.py +++ b/tests/auxil/dummy_objects.py @@ -4,6 +4,7 @@ from telegram import ( AcceptedGiftTypes, + BotAccessSettings, BotCommand, BotDescription, BotName, @@ -63,6 +64,7 @@ _PREPARED_DUMMY_OBJECTS: dict[str, object] = { "bool": True, + "BotAccessSettings": BotAccessSettings(is_access_restricted=True, added_users=[_DUMMY_USER]), "BotCommand": BotCommand(command="dummy_command", description="dummy_description"), "BotDescription": BotDescription(description="dummy_description"), "BotName": BotName(name="dummy_name"), diff --git a/tests/test_bot.py b/tests/test_bot.py index 15673347538..8d5296d01cd 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -33,6 +33,7 @@ from telegram import ( Bot, + BotAccessSettings, BotCommand, BotCommandScopeChat, BotDescription, @@ -2920,6 +2921,42 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): ) assert isinstance(inst, PreparedKeyboardButton) + async def test_get_managed_bot_access_settings(self, offline_bot, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("user_id") == 1234 + return BotAccessSettings( + is_access_restricted=True, + added_users=[User(1, "first", False)], + ).to_dict() + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + settings = await offline_bot.get_managed_bot_access_settings(1234) + assert isinstance(settings, BotAccessSettings) + + async def test_set_managed_bot_access_settings(self, offline_bot, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("user_id") == 1234 + assert request_data.parameters.get("is_access_restricted") is True + assert request_data.parameters.get("added_user_ids") == [1, 2, 3] + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + await offline_bot.set_managed_bot_access_settings( + 1234, + is_access_restricted=True, + added_user_ids=[1, 2, 3], + ) + + async def test_get_user_personal_chat_messages(self, offline_bot, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("user_id") == 1234 + assert request_data.parameters.get("limit") == 1 + return [make_message("dummy reply").to_dict()] + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + msgs = await offline_bot.get_user_personal_chat_messages(1234, limit=1) + assert isinstance(msgs, tuple) + assert all(isinstance(msg, Message) for msg in msgs) + # Bots cannot delete their own reaction from my testing, so we aren't making a real request async def test_delete_message_reaction(self, offline_bot, monkeypatch): async def make_assertion(url, request_data: RequestData, *args, **kwargs): @@ -4948,6 +4985,12 @@ async def test_my_profile_photo(self, bot): bot_profile_photos = await bot.get_user_profile_photos(bot.id) assert bot_profile_photos.total_count == 1 + async def test_get_user_personal_chat_messages(self, bot): + # id is of the Test User + messages = await bot.get_user_personal_chat_messages(user_id=675666224, limit=2) + assert isinstance(messages, tuple) + assert len(messages) == 2 + async def test_initialize_tracks_requests_and_bot_separately(self, offline_bot, monkeypatch): """Test that requests and bot user are initialized separately and only once.""" request_init_count = 0 diff --git a/tests/test_botaccesssettings.py b/tests/test_botaccesssettings.py new file mode 100644 index 00000000000..56cb8d05f50 --- /dev/null +++ b/tests/test_botaccesssettings.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# 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 Telegram Bot Access Settings.""" + +import pytest + +from telegram import BotAccessSettings, Dice +from telegram._user import User +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def bot_access_settings(): + return BotAccessSettings( + is_access_restricted=BotAccessSettingsTestBase.is_access_restricted, + added_users=BotAccessSettingsTestBase.added_users, + ) + + +class BotAccessSettingsTestBase: + is_access_restricted = True + added_users = [User(id=123, first_name="John", is_bot=False)] + + +class TestBotAccessSettingsWithoutRequest(BotAccessSettingsTestBase): + def test_slot_behaviour(self, bot_access_settings): + inst = bot_access_settings + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "is_access_restricted": self.is_access_restricted, + "added_users": [user.to_dict() for user in self.added_users], + } + bot_access_settings = BotAccessSettings.de_json(json_dict, offline_bot) + assert bot_access_settings.api_kwargs == {} + + assert bot_access_settings.is_access_restricted == self.is_access_restricted + assert bot_access_settings.added_users == tuple(self.added_users) + + def test_to_dict(self, bot_access_settings): + bot_access_settings_dict = bot_access_settings.to_dict() + + assert isinstance(bot_access_settings_dict, dict) + assert ( + bot_access_settings_dict["is_access_restricted"] + == bot_access_settings.is_access_restricted + ) + assert isinstance(bot_access_settings_dict["added_users"], list) + assert bot_access_settings_dict["added_users"][0] == self.added_users[0].to_dict() + + def test_equality(self): + a = BotAccessSettings(is_access_restricted=True, added_users=self.added_users) + b = BotAccessSettings(is_access_restricted=True, added_users=self.added_users) + c = BotAccessSettings(is_access_restricted=False, added_users=self.added_users) + d = BotAccessSettings(is_access_restricted=True, added_users=None) + e = Dice(emoji="🎲", value=4) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) diff --git a/tests/test_user.py b/tests/test_user.py index 4d6d35755e5..6ebebcf00ce 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -927,6 +927,73 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(user.get_bot(), "replace_managed_bot_token", make_assertion) assert await user.replace_token() + async def test_instance_method_get_managed_bot_access_settings(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["user_id"] == user.id + + assert check_shortcut_signature( + user.get_managed_bot_access_settings, + Bot.get_managed_bot_access_settings, + ["user_id"], + [], + ) + assert await check_shortcut_call( + user.get_managed_bot_access_settings, + user.get_bot(), + "get_managed_bot_access_settings", + ) + assert await check_defaults_handling(user.get_managed_bot_access_settings, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "get_managed_bot_access_settings", make_assertion) + assert await user.get_managed_bot_access_settings() + + async def test_instance_method_set_managed_bot_access_settings(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return ( + kwargs["user_id"] == user.id + and kwargs["is_access_restricted"] is True + and kwargs["added_user_ids"] == [123] + ) + + assert check_shortcut_signature( + user.set_managed_bot_access_settings, + Bot.set_managed_bot_access_settings, + ["user_id"], + [], + ) + assert await check_shortcut_call( + user.set_managed_bot_access_settings, + user.get_bot(), + "set_managed_bot_access_settings", + ) + assert await check_defaults_handling(user.set_managed_bot_access_settings, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "set_managed_bot_access_settings", make_assertion) + assert await user.set_managed_bot_access_settings( + is_access_restricted=True, + added_user_ids=[123], + ) + + async def test_instance_method_get_personal_chat_messages(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["user_id"] == user.id and kwargs["limit"] == 2 + + assert check_shortcut_signature( + user.get_personal_chat_messages, + Bot.get_user_personal_chat_messages, + ["user_id"], + [], + ) + assert await check_shortcut_call( + user.get_personal_chat_messages, + user.get_bot(), + "get_user_personal_chat_messages", + ) + assert await check_defaults_handling(user.get_personal_chat_messages, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "get_user_personal_chat_messages", make_assertion) + assert await user.get_personal_chat_messages(limit=2) + async def test_instance_method_delete_reaction(self, monkeypatch, user): async def make_assertion(*_, **kwargs): return (