From 1fbac179dbc5586b0ad462d3dd1dba8a50f9b841 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 31 Mar 2024 16:24:03 -0400 Subject: [PATCH 01/30] Add new parameter to bot methods and 2 new business params in Message --- docs/substitutions/global.rst | 2 + pyproject.toml | 2 +- telegram/_bot.py | 86 +++++++++++++++++++++++++++++++++++ telegram/_chat.py | 34 ++++++++++++++ telegram/_message.py | 73 +++++++++++++++++++++++++++++ telegram/_user.py | 34 ++++++++++++++ telegram/constants.py | 10 ++++ telegram/ext/_extbot.py | 36 +++++++++++++++ tests/test_message.py | 4 ++ 9 files changed, 280 insertions(+), 1 deletion(-) diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index 050a6d52b9e..36038e71eba 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -79,3 +79,5 @@ .. |do_quote| replace:: If set to :obj:`True`, the replied message is quoted. For a dict, it must be the output of :meth:`~telegram.Message.build_reply_arguments` to specify exact ``reply_parameters``. If ``reply_to_message_id`` or ``reply_parameters`` are passed, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. .. |non_optional_story_argument| replace:: As of this version, this argument is now required. In accordance with our `stability policy `__, the signature will be kept as optional for now, though they are mandatory and an error will be raised if you don't pass it. + +.. |business_id_str| replace:: Unique identifier of the business connection on behalf of which the message will be sent. diff --git a/pyproject.toml b/pyproject.toml index b941a244486..34c2a763798 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ explicit-preview-rules = true ignore = ["PLR2004", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PERF203"] select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE", "G", "ISC", "PT", "ASYNC", "TCH", "SLOT", "PERF", "PYI", "FLY", "AIR", "RUF022", - "RUF023", "Q", "INP",] + "RUF023", "Q", "INP", "W"] # Add "FURB" after it's out of preview [tool.ruff.lint.per-file-ignores] diff --git a/telegram/_bot.py b/telegram/_bot.py index a792e0f9490..f8365c4f317 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -667,6 +667,7 @@ async def _send_message( caption_entities: Optional[Sequence["MessageEntity"]] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -723,6 +724,9 @@ async def _send_message( if caption_entities is not None: data["caption_entities"] = caption_entities + if business_connection_id is not None: + data["business_connection_id"] = business_connection_id + result = await self._post( endpoint, data, @@ -911,6 +915,7 @@ async def send_message( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -956,6 +961,7 @@ async def send_message( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1009,6 +1015,7 @@ async def send_message( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, parse_mode=parse_mode, link_preview_options=link_preview_options, reply_parameters=reply_parameters, @@ -1259,6 +1266,7 @@ async def send_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1319,6 +1327,9 @@ async def send_photo( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1376,6 +1387,7 @@ async def send_photo( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_audio( @@ -1394,6 +1406,7 @@ async def send_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1463,6 +1476,9 @@ async def send_audio( reply_parameters (:obj:`ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1539,6 +1555,7 @@ async def send_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1607,6 +1624,9 @@ async def send_document( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1663,6 +1683,7 @@ async def send_document( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_sticker( @@ -1675,6 +1696,7 @@ async def send_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1723,6 +1745,9 @@ async def send_sticker( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1771,6 +1796,7 @@ async def send_sticker( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_video( @@ -1791,6 +1817,7 @@ async def send_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1868,6 +1895,9 @@ async def send_video( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1930,6 +1960,7 @@ async def send_video( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_video_note( @@ -1944,6 +1975,7 @@ async def send_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2006,6 +2038,9 @@ async def send_video_note( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2062,6 +2097,7 @@ async def send_video_note( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_animation( @@ -2081,6 +2117,7 @@ async def send_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2152,6 +2189,9 @@ async def send_animation( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2213,6 +2253,7 @@ async def send_animation( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_voice( @@ -2228,6 +2269,7 @@ async def send_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2292,6 +2334,9 @@ async def send_voice( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2349,6 +2394,7 @@ async def send_voice( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_media_group( @@ -2361,6 +2407,7 @@ async def send_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2408,6 +2455,9 @@ async def send_media_group( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2500,6 +2550,7 @@ async def send_media_group( "protect_content": protect_content, "message_thread_id": message_thread_id, "reply_parameters": reply_parameters, + "business_connection_id": business_connection_id, } result = await self._post( @@ -2528,6 +2579,7 @@ async def send_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2579,6 +2631,9 @@ async def send_location( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2647,6 +2702,7 @@ async def send_location( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def edit_message_live_location( @@ -2806,6 +2862,7 @@ async def send_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2855,6 +2912,9 @@ async def send_venue( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2934,6 +2994,7 @@ async def send_venue( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_contact( @@ -2948,6 +3009,7 @@ async def send_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2987,6 +3049,9 @@ async def send_contact( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -3057,6 +3122,7 @@ async def send_contact( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_game( @@ -3068,6 +3134,7 @@ async def send_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3097,6 +3164,9 @@ async def send_game( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -3142,6 +3212,7 @@ async def send_game( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_chat_action( @@ -3149,6 +3220,7 @@ async def send_chat_action( chat_id: Union[str, int], action: str, message_thread_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3170,6 +3242,9 @@ async def send_chat_action( message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -3182,6 +3257,7 @@ async def send_chat_action( "chat_id": chat_id, "action": action, "message_thread_id": message_thread_id, + "business_connection_id": business_connection_id, } return await self._post( "sendChatAction", @@ -6728,6 +6804,7 @@ async def send_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -6803,6 +6880,9 @@ async def send_poll( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -6862,6 +6942,7 @@ async def send_poll( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def stop_poll( @@ -6918,6 +6999,7 @@ async def send_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -6961,6 +7043,9 @@ async def send_dice( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -7007,6 +7092,7 @@ async def send_dice( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def get_my_default_administrator_rights( diff --git a/telegram/_chat.py b/telegram/_chat.py index 915614bb6c8..741b0650d0c 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -1444,6 +1444,7 @@ async def send_message( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1483,6 +1484,7 @@ async def send_message( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def delete_message( @@ -1558,6 +1560,7 @@ async def send_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1598,12 +1601,14 @@ async def send_media_group( parse_mode=parse_mode, caption_entities=caption_entities, reply_parameters=reply_parameters, + business_connection_id=business_connection_id, ) async def send_chat_action( self, action: str, message_thread_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1630,6 +1635,7 @@ async def send_chat_action( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) send_action = send_chat_action @@ -1647,6 +1653,7 @@ async def send_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1687,6 +1694,7 @@ async def send_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, has_spoiler=has_spoiler, + business_connection_id=business_connection_id, ) async def send_contact( @@ -1700,6 +1708,7 @@ async def send_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1739,6 +1748,7 @@ async def send_contact( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_audio( @@ -1756,6 +1766,7 @@ async def send_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1799,6 +1810,7 @@ async def send_audio( pool_timeout=pool_timeout, api_kwargs=api_kwargs, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_document( @@ -1814,6 +1826,7 @@ async def send_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1855,6 +1868,7 @@ async def send_document( caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_dice( @@ -1865,6 +1879,7 @@ async def send_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1899,6 +1914,7 @@ async def send_dice( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_game( @@ -1909,6 +1925,7 @@ async def send_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1943,6 +1960,7 @@ async def send_game( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_invoice( @@ -2052,6 +2070,7 @@ async def send_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2093,6 +2112,7 @@ async def send_location( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_animation( @@ -2111,6 +2131,7 @@ async def send_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2155,6 +2176,7 @@ async def send_animation( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_sticker( @@ -2166,6 +2188,7 @@ async def send_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2201,6 +2224,7 @@ async def send_sticker( protect_content=protect_content, message_thread_id=message_thread_id, emoji=emoji, + business_connection_id=business_connection_id, ) async def send_venue( @@ -2218,6 +2242,7 @@ async def send_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2261,6 +2286,7 @@ async def send_venue( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_video( @@ -2280,6 +2306,7 @@ async def send_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2325,6 +2352,7 @@ async def send_video( protect_content=protect_content, message_thread_id=message_thread_id, has_spoiler=has_spoiler, + business_connection_id=business_connection_id, ) async def send_video_note( @@ -2338,6 +2366,7 @@ async def send_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2377,6 +2406,7 @@ async def send_video_note( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_voice( @@ -2391,6 +2421,7 @@ async def send_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2431,6 +2462,7 @@ async def send_voice( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_poll( @@ -2452,6 +2484,7 @@ async def send_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2497,6 +2530,7 @@ async def send_poll( explanation_entities=explanation_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_copy( diff --git a/telegram/_message.py b/telegram/_message.py index 5e2f6024709..00debdce501 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Message.""" + import datetime import re from html import escape @@ -534,6 +535,18 @@ class Message(MaybeInaccessibleMessage): message boosted the chat, the number of boosts added by the user. .. versionadded:: 21.0 + business_connection_id (:obj:`str`, optional): Unique identifier of the business connection + from which the message was received. If non-empty, the message belongs to a chat of the + corresponding business account that is independent from any potential bot chat which + might share the same identifier. + + .. versionadded:: NEXT.VERSION + + sender_business_bot (:obj:`str`, optional): The bot that actually sent the message on + behalf of the business account. Available only for outgoing messages sent on behalf of + the connected business account. + + .. versionadded:: NEXT.VERSION Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -817,6 +830,19 @@ class Message(MaybeInaccessibleMessage): .. versionadded:: 21.0 + business_connection_id (:obj:`str`): Optional. Unique identifier of the business connection + from which the message was received. If non-empty, the message belongs to a chat of the + corresponding business account that is independent from any potential bot chat which + might share the same identifier. + + .. versionadded:: NEXT.VERSION + + sender_business_bot (:obj:`str`): Optional. The bot that actually sent the message on + behalf of the business account. Available only for outgoing messages sent on behalf of + the connected business account. + + .. versionadded:: NEXT.VERSION + .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a :exc:`ValueError` when encountering a custom emoji. @@ -833,6 +859,7 @@ class Message(MaybeInaccessibleMessage): "audio", "author_signature", "boost_added", + "business_connection_id", "caption", "caption_entities", "channel_chat_created", @@ -885,6 +912,7 @@ class Message(MaybeInaccessibleMessage): "reply_to_message", "reply_to_story", "sender_boost_count", + "sender_business_bot", "sender_chat", "sticker", "story", @@ -984,6 +1012,8 @@ def __init__( reply_to_story: Optional[Story] = None, boost_added: Optional[ChatBoostAdded] = None, sender_boost_count: Optional[int] = None, + business_connection_id: Optional[str] = None, + sender_business_bot: Optional[User] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1079,6 +1109,8 @@ def __init__( self.reply_to_story: Optional[Story] = reply_to_story self.boost_added: Optional[ChatBoostAdded] = boost_added self.sender_boost_count: Optional[int] = sender_boost_count + self.business_connection_id: Optional[str] = business_connection_id + self.sender_business_bot: Optional[User] = sender_business_bot self._effective_attachment = DEFAULT_NONE @@ -1221,6 +1253,7 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Message"]: data["forward_origin"] = MessageOrigin.de_json(data.get("forward_origin"), bot) data["reply_to_story"] = Story.de_json(data.get("reply_to_story"), bot) data["boost_added"] = ChatBoostAdded.de_json(data.get("boost_added"), bot) + data["sender_business_bot"] = User.de_json(data.get("sender_business_bot"), bot) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility @@ -1546,6 +1579,7 @@ async def reply_text( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1599,6 +1633,7 @@ async def reply_text( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def reply_markdown( @@ -1611,6 +1646,7 @@ async def reply_markdown( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1674,6 +1710,7 @@ async def reply_markdown( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def reply_markdown_v2( @@ -1686,6 +1723,7 @@ async def reply_markdown_v2( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1745,6 +1783,7 @@ async def reply_markdown_v2( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def reply_html( @@ -1757,6 +1796,7 @@ async def reply_html( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1816,6 +1856,7 @@ async def reply_html( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def reply_media_group( @@ -1827,6 +1868,7 @@ async def reply_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1882,6 +1924,7 @@ async def reply_media_group( caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + business_connection_id=business_connection_id, ) async def reply_photo( @@ -1896,6 +1939,7 @@ async def reply_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1950,6 +1994,7 @@ async def reply_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, has_spoiler=has_spoiler, + business_connection_id=business_connection_id, ) async def reply_audio( @@ -1967,6 +2012,7 @@ async def reply_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2024,6 +2070,7 @@ async def reply_audio( pool_timeout=pool_timeout, api_kwargs=api_kwargs, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def reply_document( @@ -2039,6 +2086,7 @@ async def reply_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2094,6 +2142,7 @@ async def reply_document( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def reply_animation( @@ -2112,6 +2161,7 @@ async def reply_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2170,6 +2220,7 @@ async def reply_animation( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def reply_sticker( @@ -2181,6 +2232,7 @@ async def reply_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2230,6 +2282,7 @@ async def reply_sticker( protect_content=protect_content, message_thread_id=message_thread_id, emoji=emoji, + business_connection_id=business_connection_id, ) async def reply_video( @@ -2249,6 +2302,7 @@ async def reply_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2308,6 +2362,7 @@ async def reply_video( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def reply_video_note( @@ -2321,6 +2376,7 @@ async def reply_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2374,6 +2430,7 @@ async def reply_video_note( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def reply_voice( @@ -2388,6 +2445,7 @@ async def reply_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2442,6 +2500,7 @@ async def reply_voice( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def reply_location( @@ -2457,6 +2516,7 @@ async def reply_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2512,6 +2572,7 @@ async def reply_location( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def reply_venue( @@ -2529,6 +2590,7 @@ async def reply_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2586,6 +2648,7 @@ async def reply_venue( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def reply_contact( @@ -2599,6 +2662,7 @@ async def reply_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2652,6 +2716,7 @@ async def reply_contact( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def reply_poll( @@ -2673,6 +2738,7 @@ async def reply_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2732,6 +2798,7 @@ async def reply_poll( explanation_entities=explanation_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def reply_dice( @@ -2742,6 +2809,7 @@ async def reply_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2790,12 +2858,14 @@ async def reply_dice( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def reply_chat_action( self, action: str, message_thread_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2824,6 +2894,7 @@ async def reply_chat_action( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def reply_game( @@ -2834,6 +2905,7 @@ async def reply_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2884,6 +2956,7 @@ async def reply_game( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def reply_invoice( diff --git a/telegram/_user.py b/telegram/_user.py index eb4227e18ce..19835da245f 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -393,6 +393,7 @@ async def send_message( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, disable_web_page_preview: Optional[bool] = None, @@ -435,6 +436,7 @@ async def send_message( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def delete_message( @@ -513,6 +515,7 @@ async def send_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -556,6 +559,7 @@ async def send_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, has_spoiler=has_spoiler, + business_connection_id=business_connection_id, ) async def send_media_group( @@ -567,6 +571,7 @@ async def send_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -610,6 +615,7 @@ async def send_media_group( caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + business_connection_id=business_connection_id, ) async def send_audio( @@ -627,6 +633,7 @@ async def send_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -673,12 +680,14 @@ async def send_audio( pool_timeout=pool_timeout, api_kwargs=api_kwargs, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_chat_action( self, action: str, message_thread_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -708,6 +717,7 @@ async def send_chat_action( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) send_action = send_chat_action @@ -724,6 +734,7 @@ async def send_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -766,6 +777,7 @@ async def send_contact( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_dice( @@ -776,6 +788,7 @@ async def send_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -813,6 +826,7 @@ async def send_dice( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_document( @@ -828,6 +842,7 @@ async def send_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -872,6 +887,7 @@ async def send_document( caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_game( @@ -882,6 +898,7 @@ async def send_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -919,6 +936,7 @@ async def send_game( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_invoice( @@ -1031,6 +1049,7 @@ async def send_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1075,6 +1094,7 @@ async def send_location( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_animation( @@ -1093,6 +1113,7 @@ async def send_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1140,6 +1161,7 @@ async def send_animation( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_sticker( @@ -1151,6 +1173,7 @@ async def send_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1189,6 +1212,7 @@ async def send_sticker( protect_content=protect_content, message_thread_id=message_thread_id, emoji=emoji, + business_connection_id=business_connection_id, ) async def send_video( @@ -1208,6 +1232,7 @@ async def send_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1256,6 +1281,7 @@ async def send_video( protect_content=protect_content, message_thread_id=message_thread_id, has_spoiler=has_spoiler, + business_connection_id=business_connection_id, ) async def send_venue( @@ -1273,6 +1299,7 @@ async def send_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1319,6 +1346,7 @@ async def send_venue( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_video_note( @@ -1332,6 +1360,7 @@ async def send_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1374,6 +1403,7 @@ async def send_video_note( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_voice( @@ -1388,6 +1418,7 @@ async def send_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1431,6 +1462,7 @@ async def send_voice( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_poll( @@ -1452,6 +1484,7 @@ async def send_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1500,6 +1533,7 @@ async def send_poll( explanation_entities=explanation_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_copy( diff --git a/telegram/constants.py b/telegram/constants.py index 959e99ac454..f75dc9344da 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -1710,6 +1710,11 @@ class MessageType(StringEnum): .. versionadded:: 21.0 """ + BUSINESS_CONNECTION_ID = "business_connection_id" + """:obj:`str`: Messages with :attr:`telegram.Message.business_connection_id`. + + .. versionadded:: NEXT.VERSION + """ CHANNEL_CHAT_CREATED = "channel_chat_created" """:obj:`str`: Messages with :attr:`telegram.Message.channel_chat_created`.""" CHAT_SHARED = "chat_shared" @@ -1817,6 +1822,11 @@ class MessageType(StringEnum): .. versionadded:: 21.0 """ + SENDER_BUSINESS_BOT = "sender_business_bot" + """:obj:`str`: Messages with :attr:`telegram.Message.sender_business_bot`. + + .. versionadded:: NEXT.VERSION + """ STICKER = "sticker" """:obj:`str`: Messages with :attr:`telegram.Message.sticker`.""" STORY = "story" diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 30ae0029999..3ba1430490c 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -571,6 +571,7 @@ async def _send_message( caption_entities: Optional[Sequence["MessageEntity"]] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -601,6 +602,7 @@ async def _send_message( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) if isinstance(result, Message): self._insert_callback_data(result) @@ -2355,6 +2357,7 @@ async def send_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2388,6 +2391,7 @@ async def send_animation( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, + business_connection_id=business_connection_id, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) @@ -2408,6 +2412,7 @@ async def send_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2424,6 +2429,7 @@ async def send_audio( audio=audio, duration=duration, performer=performer, + business_connection_id=business_connection_id, title=title, caption=caption, disable_notification=disable_notification, @@ -2449,6 +2455,7 @@ async def send_chat_action( chat_id: Union[str, int], action: str, message_thread_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2459,6 +2466,7 @@ async def send_chat_action( ) -> bool: return await super().send_chat_action( chat_id=chat_id, + business_connection_id=business_connection_id, action=action, message_thread_id=message_thread_id, read_timeout=read_timeout, @@ -2480,6 +2488,7 @@ async def send_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2509,6 +2518,7 @@ async def send_contact( write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, + business_connection_id=business_connection_id, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) @@ -2521,6 +2531,7 @@ async def send_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2534,6 +2545,7 @@ async def send_dice( return await super().send_dice( chat_id=chat_id, disable_notification=disable_notification, + business_connection_id=business_connection_id, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, emoji=emoji, @@ -2562,6 +2574,7 @@ async def send_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2585,6 +2598,7 @@ async def send_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + business_connection_id=business_connection_id, message_thread_id=message_thread_id, thumbnail=thumbnail, reply_parameters=reply_parameters, @@ -2605,6 +2619,7 @@ async def send_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2621,6 +2636,7 @@ async def send_game( disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, + business_connection_id=business_connection_id, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, @@ -2722,6 +2738,7 @@ async def send_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2752,6 +2769,7 @@ async def send_location( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, + business_connection_id=business_connection_id, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) @@ -2766,6 +2784,7 @@ async def send_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2794,6 +2813,7 @@ async def send_media_group( pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), caption=caption, + business_connection_id=business_connection_id, parse_mode=parse_mode, caption_entities=caption_entities, ) @@ -2810,6 +2830,7 @@ async def send_message( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, disable_web_page_preview: Optional[bool] = None, reply_to_message_id: Optional[int] = None, @@ -2828,6 +2849,7 @@ async def send_message( entities=entities, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, + business_connection_id=business_connection_id, protect_content=protect_content, message_thread_id=message_thread_id, reply_to_message_id=reply_to_message_id, @@ -2855,6 +2877,7 @@ async def send_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2881,6 +2904,7 @@ async def send_photo( has_spoiler=has_spoiler, reply_parameters=reply_parameters, filename=filename, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2908,6 +2932,7 @@ async def send_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2936,6 +2961,7 @@ async def send_poll( close_date=close_date, allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, + business_connection_id=business_connection_id, protect_content=protect_content, message_thread_id=message_thread_id, reply_parameters=reply_parameters, @@ -2956,6 +2982,7 @@ async def send_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2972,6 +2999,7 @@ async def send_sticker( disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, + business_connection_id=business_connection_id, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, @@ -3000,6 +3028,7 @@ async def send_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3026,6 +3055,7 @@ async def send_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + business_connection_id=business_connection_id, message_thread_id=message_thread_id, reply_parameters=reply_parameters, venue=venue, @@ -3054,6 +3084,7 @@ async def send_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3081,6 +3112,7 @@ async def send_video( caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, has_spoiler=has_spoiler, thumbnail=thumbnail, filename=filename, @@ -3104,6 +3136,7 @@ async def send_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3134,6 +3167,7 @@ async def send_video_note( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + business_connection_id=business_connection_id, ) async def send_voice( @@ -3149,6 +3183,7 @@ async def send_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3180,6 +3215,7 @@ async def send_voice( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + business_connection_id=business_connection_id, ) async def set_chat_administrator_custom_title( diff --git a/tests/test_message.py b/tests/test_message.py index b98f36b4577..dc2a0bfe15f 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -263,6 +263,8 @@ def message(bot): {"reply_to_story": Story(Chat(1, Chat.PRIVATE), 0)}, {"boost_added": ChatBoostAdded(100)}, {"sender_boost_count": 1}, + {"sender_business_bot": User(1, "BusinessBot", True)}, + {"business_connection_id": "123456789"}, ], ids=[ "reply", @@ -328,6 +330,8 @@ def message(bot): "reply_to_story", "boost_added", "sender_boost_count", + "sender_business_bot", + "business_connection_id", ], ) def message_params(bot, request): From 4b0e7fc4d9c22b6992b8ae916df495b9bbbc9c89 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 31 Mar 2024 16:25:39 -0400 Subject: [PATCH 02/30] Remove whitespace detected by ruff --- telegram/_passport/encryptedpassportelement.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/telegram/_passport/encryptedpassportelement.py b/telegram/_passport/encryptedpassportelement.py index 14d8b63dcf5..e6a22ee2e7e 100644 --- a/telegram/_passport/encryptedpassportelement.py +++ b/telegram/_passport/encryptedpassportelement.py @@ -60,8 +60,8 @@ class EncryptedPassportElement(TelegramObject): email (:obj:`str`, optional): User's verified email address; available only for "email" type. files (Sequence[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted - files with documents provided by the user; available only for "utility_bill", - "bank_statement", "rental_agreement", "passport_registration" and + files with documents provided by the user; available only for "utility_bill", + "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 @@ -74,12 +74,12 @@ class EncryptedPassportElement(TelegramObject): reverse side of the document, provided by the user; Available only for "driver_license" and "identity_card". selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the - selfie of the user holding a document, provided by the user; available if requested for + selfie of the user holding a document, provided by the user; available if requested for "passport", "driver_license", "identity_card" and "internal_passport". translation (Sequence[:class:`telegram.PassportFile`], optional): Array of - encrypted/decrypted files with translated versions of documents provided by the user; - available if requested requested for "passport", "driver_license", "identity_card", - "internal_passport", "utility_bill", "bank_statement", "rental_agreement", + encrypted/decrypted files with translated versions of documents provided by the user; + available if requested requested for "passport", "driver_license", "identity_card", + "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 @@ -101,8 +101,8 @@ class EncryptedPassportElement(TelegramObject): email (:obj:`str`): Optional. User's verified email address; available only for "email" type. files (Tuple[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted - files with documents provided by the user; available only for "utility_bill", - "bank_statement", "rental_agreement", "passport_registration" and + files with documents provided by the user; available only for "utility_bill", + "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 From af341cc5eb21730556ad430f5eabc10a0e6db503 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 31 Mar 2024 16:27:05 -0400 Subject: [PATCH 03/30] Bump bot api version number --- README.rst | 4 ++-- README_RAW.rst | 4 ++-- telegram/constants.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 8366a1ff6f0..1d6b20aafea 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.1-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -89,7 +89,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 **7.1** are supported. +All types and methods of the Telegram Bot API **7.2** are supported. Installing ========== diff --git a/README_RAW.rst b/README_RAW.rst index fae3d516e38..df1312e4857 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.1-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -85,7 +85,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 **7.1** are supported. +All types and methods of the Telegram Bot API **7.2** are supported. Installing ========== diff --git a/telegram/constants.py b/telegram/constants.py index f75dc9344da..378862ff152 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -142,7 +142,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=1) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=2) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. From 735ed1e29daeed201c9caea2aa554a274e106763 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 31 Mar 2024 16:40:17 -0400 Subject: [PATCH 04/30] fix pre-commit --- telegram/_bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index f8365c4f317..b606d5414ea 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# pylint: disable=no-self-argument, not-callable, no-member, too-many-arguments +# pylint: disable=too-many-arguments # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2024 @@ -1539,6 +1539,7 @@ async def send_audio( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def send_document( From 9c35dbdd6d0d8b9676e56b1ed8964f09aa060f78 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 31 Mar 2024 19:55:25 -0400 Subject: [PATCH 05/30] doc fix --- telegram/_message.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/telegram/_message.py b/telegram/_message.py index 00debdce501..62848565a3d 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -542,9 +542,9 @@ class Message(MaybeInaccessibleMessage): .. versionadded:: NEXT.VERSION - sender_business_bot (:obj:`str`, optional): The bot that actually sent the message on - behalf of the business account. Available only for outgoing messages sent on behalf of - the connected business account. + sender_business_bot (:obj:`telegram.User`, optional): The bot that actually sent the + message on behalf of the business account. Available only for outgoing messages sent + on behalf of the connected business account. .. versionadded:: NEXT.VERSION @@ -837,9 +837,9 @@ class Message(MaybeInaccessibleMessage): .. versionadded:: NEXT.VERSION - sender_business_bot (:obj:`str`): Optional. The bot that actually sent the message on - behalf of the business account. Available only for outgoing messages sent on behalf of - the connected business account. + sender_business_bot (:obj:`telegram.User`): Optional. The bot that actually sent the + message on behalf of the business account. Available only for outgoing messages sent + on behalf of the connected business account. .. versionadded:: NEXT.VERSION From badf3fcb74d252fe800a7f2ca8af9a3adfa4c116 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:44:15 -0400 Subject: [PATCH 06/30] Add new parameters to Update along with the new method --- docs/source/inclusions/bot_methods.rst | 10 +- docs/source/telegram.at-tree.rst | 2 + docs/source/telegram.businessconnection.rst | 6 + .../telegram.businessmessagesdeleted.rst | 6 + telegram/__init__.py | 3 + telegram/_bot.py | 42 ++++ telegram/_business.py | 185 ++++++++++++++++++ telegram/_update.py | 119 ++++++++++- telegram/constants.py | 20 ++ tests/test_bot.py | 5 + tests/test_business.py | 172 ++++++++++++++++ tests/test_update.py | 27 +++ 12 files changed, 590 insertions(+), 7 deletions(-) create mode 100644 docs/source/telegram.businessconnection.rst create mode 100644 docs/source/telegram.businessmessagesdeleted.rst create mode 100644 telegram/_business.py create mode 100644 tests/test_business.py diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 1f05f11ff11..11b390bab32 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -113,6 +113,10 @@ :align: left :widths: 1 4 + * - :meth:`~telegram.Bot.approve_chat_join_request` + - Used for approving a chat join request + * - :meth:`~telegram.Bot.decline_chat_join_request` + - Used for declining a chat join request * - :meth:`~telegram.Bot.ban_chat_member` - Used for banning a member from the chat * - :meth:`~telegram.Bot.unban_chat_member` @@ -137,10 +141,6 @@ - Used for editing a non-primary invite link * - :meth:`~telegram.Bot.revoke_chat_invite_link` - Used for revoking an invite link created by the bot - * - :meth:`~telegram.Bot.approve_chat_join_request` - - Used for approving a chat join request - * - :meth:`~telegram.Bot.decline_chat_join_request` - - Used for declining a chat join request * - :meth:`~telegram.Bot.set_chat_photo` - Used for setting a photo to a chat * - :meth:`~telegram.Bot.delete_chat_photo` @@ -155,6 +155,8 @@ - Used for unpinning a message * - :meth:`~telegram.Bot.unpin_all_chat_messages` - Used for unpinning all pinned chat messages + * - :meth:`~telegram.Bot.get_business_connection` + - Used for getting information about the business account. * - :meth:`~telegram.Bot.get_user_profile_photos` - Used for obtaining user's profile pictures * - :meth:`~telegram.Bot.get_chat` diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index ffa7107b89e..19b9831cc76 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -18,6 +18,8 @@ Available Types telegram.botdescription telegram.botname telegram.botshortdescription + telegram.businessconnection + telegram.businessmessagesdeleted telegram.callbackquery telegram.chat telegram.chatadministratorrights diff --git a/docs/source/telegram.businessconnection.rst b/docs/source/telegram.businessconnection.rst new file mode 100644 index 00000000000..3ef31c3b25e --- /dev/null +++ b/docs/source/telegram.businessconnection.rst @@ -0,0 +1,6 @@ +BusinessConnection +================== + +.. autoclass:: telegram.BusinessConnection + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.businessmessagesdeleted.rst b/docs/source/telegram.businessmessagesdeleted.rst new file mode 100644 index 00000000000..ba0e88e3cba --- /dev/null +++ b/docs/source/telegram.businessmessagesdeleted.rst @@ -0,0 +1,6 @@ +BusinessMessagesDeleted +======================= + +.. autoclass:: telegram.BusinessMessagesDeleted + :members: + :show-inheritance: \ No newline at end of file diff --git a/telegram/__init__.py b/telegram/__init__.py index 162ba3d0edb..6b9d513b361 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -35,6 +35,8 @@ "BotDescription", "BotName", "BotShortDescription", + "BusinessConnection", + "BusinessMessagesDeleted", "CallbackGame", "CallbackQuery", "Chat", @@ -238,6 +240,7 @@ ) from ._botdescription import BotDescription, BotShortDescription from ._botname import BotName +from ._business import BusinessConnection, BusinessMessagesDeleted from ._callbackquery import CallbackQuery from ._chat import Chat from ._chatadministratorrights import ChatAdministratorRights diff --git a/telegram/_bot.py b/telegram/_bot.py index b606d5414ea..e66b433b81e 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -57,6 +57,7 @@ from telegram._botcommandscope import BotCommandScope from telegram._botdescription import BotDescription, BotShortDescription from telegram._botname import BotName +from telegram._business import BusinessConnection from telegram._chat import Chat from telegram._chatadministratorrights import ChatAdministratorRights from telegram._chatboost import UserChatBoosts @@ -8752,6 +8753,45 @@ async def set_message_reaction( api_kwargs=api_kwargs, ) + async def get_business_connection( + self, + business_connection_id: str, + *, + 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: Optional[JSONDict] = None, + ) -> BusinessConnection: + """ + Use this method to get information about the connection of the bot with a business account. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + + Returns: + :class:`telegram.BusinessConnection`: On success, the object containing the business + connection information is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"business_connection_id": business_connection_id} + return BusinessConnection.de_json( # type: ignore[return-value] + await self._post( + "getBusinessConnection", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -8998,3 +9038,5 @@ def to_dict(self, recursive: bool = True) -> JSONDict: """Alias for :meth:`get_user_chat_boosts`""" setMessageReaction = set_message_reaction """Alias for :meth:`set_message_reaction`""" + getBusinessConnection = get_business_connection + """Alias for :meth:`get_business_connection`""" diff --git a/telegram/_business.py b/telegram/_business.py new file mode 100644 index 00000000000..e7ec28855b9 --- /dev/null +++ b/telegram/_business.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +# pylint: disable=redefined-builtin +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the Telegram Business related classes.""" + +from datetime import datetime +from typing import TYPE_CHECKING, Optional, Sequence + +from telegram._chat import Chat +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class BusinessConnection(TelegramObject): + """ + Describes the connection of the bot with a business account. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`id`, :attr:`user`, :attr:`user_chat_id`, :attr:`date`, + :attr:`can_reply`, and :attr:`is_enabled` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + id (:obj:`str`): Unique identifier of the business connection. + user (:class:`telegram.User`): Business account user that created the business connection. + user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the + business connection. + date (:obj:`datetime.datetime`): Date the connection was established in Unix time. + can_reply (:obj:`bool`): True, if the bot can act on behalf of the business account in + chats that were active in the last 24 hours. + is_enabled (:obj:`bool`): True, if the connection is active. + + Attributes: + id (:obj:`str`): Unique identifier of the business connection. + user (:class:`telegram.User`): Business account user that created the business connection. + user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the + business connection. + date (:obj:`datetime.datetime`): Date the connection was established in Unix time. + can_reply (:obj:`bool`): True, if the bot can act on behalf of the business account in + chats that were active in the last 24 hours. + is_enabled (:obj:`bool`): True, if the connection is active. + """ + + __slots__ = ( + "can_reply", + "date", + "id", + "is_enabled", + "user", + "user_chat_id", + ) + + def __init__( + self, + id: str, + user: "User", + user_chat_id: int, + date: datetime, + can_reply: bool, + is_enabled: bool, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.id = id + self.user = user + self.user_chat_id = user_chat_id + self.date = date + self.can_reply = can_reply + self.is_enabled = is_enabled + + self._id_attrs = ( + self.id, + self.user, + self.user_chat_id, + self.date, + self.can_reply, + self.is_enabled, + ) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessConnection"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + + data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) + data["user"] = User.de_json(data.get("user"), bot) + + return super().de_json(data=data, bot=bot) + + +class BusinessMessagesDeleted(TelegramObject): + """ + This object is received when messages are deleted from a connected business account. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`business_connection_id`, :attr:`message_ids`, and + :attr:`chat` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + chat (:class:`telegram.Chat`): Information about a chat in the business account. The bot + may not have access to the chat or the corresponding user. + message_ids (Sequence[:obj:`int`]): A list of identifiers of the deleted messages in the + chat of the business account. + + Attributes: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + chat (:class:`telegram.Chat`): Information about a chat in the business account. The bot + may not have access to the chat or the corresponding user. + message_ids (Sequence[:obj:`int`]): A list of identifiers of the deleted messages in the + chat of the business account. + """ + + __slots__ = ( + "business_connection_id", + "chat", + "message_ids", + ) + + def __init__( + self, + business_connection_id: str, + chat: Chat, + message_ids: Sequence[int], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.business_connection_id = business_connection_id + self.chat = chat + self.message_ids = parse_sequence_arg(message_ids) + + self._id_attrs = ( + self.business_connection_id, + self.chat, + self.message_ids, + ) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessMessagesDeleted"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["chat"] = Chat.de_json(data.get("chat"), bot) + + return super().de_json(data=data, bot=bot) diff --git a/telegram/_update.py b/telegram/_update.py index a597f7792f8..57fb34e0344 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Final, List, Optional, Union from telegram import constants +from telegram._business import BusinessConnection, BusinessMessagesDeleted from telegram._callbackquery import CallbackQuery from telegram._chatboost import ChatBoostRemoved, ChatBoostUpdated from telegram._chatjoinrequest import ChatJoinRequest @@ -134,6 +135,28 @@ class Update(TelegramObject): .. versionadded:: 20.8 + business_connection (:class:`telegram.BusinessConnection`, optional): The bot was connected + to or disconnected from a business account, or a user edited an existing connection + with the bot. + + .. versionadded:: NEXT.VERSION + + business_message (:class:`telegram.Message`, optional): New non-service message + from a connected business account. + + .. versionadded:: NEXT.VERSION + + edited_business_message (:class:`telegram.Message`, optional): New version of a message + from a connected business account. + + .. versionadded:: NEXT.VERSION + + deleted_business_messages (:class:`telegram.BusinessMessagesDeleted`, optional): Messages + were deleted from a connected business account. + + .. versionadded:: NEXT.VERSION + + Attributes: update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a certain positive number and increase sequentially. This ID becomes especially handy if @@ -219,6 +242,27 @@ class Update(TelegramObject): with delay up to a few minutes. .. versionadded:: 20.8 + + business_connection (:class:`telegram.BusinessConnection`): Optional. The bot was connected + to or disconnected from a business account, or a user edited an existing connection + with the bot. + + .. versionadded:: NEXT.VERSION + + business_message (:class:`telegram.Message`): Optional. New non-service message + from a connected business account. + + .. versionadded:: NEXT.VERSION + + edited_business_message (:class:`telegram.Message`): Optional. New version of a message + from a connected business account. + + .. versionadded:: NEXT.VERSION + + deleted_business_messages (:class:`telegram.BusinessMessagesDeleted`): Optional. Messages + were deleted from a connected business account. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -226,12 +270,16 @@ class Update(TelegramObject): "_effective_message", "_effective_sender", "_effective_user", + "business_connection", + "business_message", "callback_query", "channel_post", "chat_boost", "chat_join_request", "chat_member", "chosen_inline_result", + "deleted_business_messages", + "edited_business_message", "edited_channel_post", "edited_message", "inline_query", @@ -319,6 +367,22 @@ class Update(TelegramObject): """:const:`telegram.constants.UpdateType.MESSAGE_REACTION_COUNT` .. versionadded:: 20.8""" + BUSINESS_CONNECTION: Final[str] = constants.UpdateType.BUSINESS_CONNECTION + """:const:`telegram.constants.UpdateType.BUSINESS_CONNECTION` + + .. versionadded:: NEXT.VERSION""" + BUSINESS_MESSAGE: Final[str] = constants.UpdateType.BUSINESS_MESSAGE + """:const:`telegram.constants.UpdateType.BUSINESS_MESSAGE` + + .. versionadded:: NEXT.VERSION""" + EDITED_BUSINESS_MESSAGE: Final[str] = constants.UpdateType.EDITED_BUSINESS_MESSAGE + """:const:`telegram.constants.UpdateType.EDITED_BUSINESS_MESSAGE` + + .. versionadded:: NEXT.VERSION""" + DELETED_BUSINESS_MESSAGES: Final[str] = constants.UpdateType.DELETED_BUSINESS_MESSAGES + """:const:`telegram.constants.UpdateType.DELETED_BUSINESS_MESSAGES` + + .. versionadded:: NEXT.VERSION""" ALL_TYPES: Final[List[str]] = list(constants.UpdateType) """List[:obj:`str`]: A list of all available update types. @@ -345,6 +409,10 @@ def __init__( removed_chat_boost: Optional[ChatBoostRemoved] = None, message_reaction: Optional[MessageReactionUpdated] = None, message_reaction_count: Optional[MessageReactionCountUpdated] = None, + business_connection: Optional[BusinessConnection] = None, + business_message: Optional[Message] = None, + edited_business_message: Optional[Message] = None, + deleted_business_messages: Optional[BusinessMessagesDeleted] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -370,6 +438,12 @@ def __init__( self.removed_chat_boost: Optional[ChatBoostRemoved] = removed_chat_boost self.message_reaction: Optional[MessageReactionUpdated] = message_reaction self.message_reaction_count: Optional[MessageReactionCountUpdated] = message_reaction_count + self.business_connection: Optional[BusinessConnection] = business_connection + self.business_message: Optional[Message] = business_message + self.edited_business_message: Optional[Message] = edited_business_message + self.deleted_business_messages: Optional[BusinessMessagesDeleted] = ( + deleted_business_messages + ) self._effective_user: Optional[User] = None self._effective_sender: Optional[Union["User", "Chat"]] = None @@ -393,6 +467,7 @@ def effective_user(self) -> Optional["User"]: * :attr:`chat_boost` * :attr:`removed_chat_boost` * :attr:`message_reaction_count` + * :attr:`deleted_business_messages` is present. @@ -443,6 +518,15 @@ def effective_user(self) -> Optional["User"]: elif self.message_reaction: user = self.message_reaction.user + elif self.business_message: + user = self.business_message.from_user + + elif self.edited_business_message: + user = self.edited_business_message.from_user + + elif self.business_connection: + user = self.business_connection.user + self._effective_user = user return user @@ -463,6 +547,7 @@ def effective_sender(self) -> Optional[Union["User", "Chat"]]: * :attr:`chat_boost` * :attr:`removed_chat_boost` * :attr:`message_reaction_count` + * :attr:`deleted_business_messages` is present. @@ -482,7 +567,12 @@ def effective_sender(self) -> Optional[Union["User", "Chat"]]: sender: Optional[Union["User", "Chat"]] = None if message := ( - self.message or self.edited_message or self.channel_post or self.edited_channel_post + self.message + or self.edited_message + or self.channel_post + or self.edited_channel_post + or self.business_message + or self.edited_business_message ): sender = message.sender_chat @@ -506,8 +596,8 @@ def effective_chat(self) -> Optional["Chat"]: If no chat is associated with this update, this gives :obj:`None`. This is the case, if :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, - :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll` or - :attr:`poll_answer` is present. + :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, + :attr:`poll_answer`, or :attr:`business_connection` is present. Example: If :attr:`message` is present, this will give :attr:`telegram.Message.chat`. @@ -554,6 +644,15 @@ def effective_chat(self) -> Optional["Chat"]: elif self.message_reaction_count: chat = self.message_reaction_count.chat + elif self.business_message: + chat = self.business_message.chat + + elif self.edited_business_message: + chat = self.edited_business_message.chat + + elif self.deleted_business_messages: + chat = self.deleted_business_messages.chat + self._effective_chat = chat return chat @@ -608,6 +707,12 @@ def effective_message(self) -> Optional[Message]: elif self.edited_channel_post: message = self.edited_channel_post + elif self.business_message: + message = self.business_message + + elif self.edited_business_message: + message = self.edited_business_message + self._effective_message = message return message @@ -643,5 +748,13 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Update"]: data["message_reaction_count"] = MessageReactionCountUpdated.de_json( data.get("message_reaction_count"), bot ) + data["business_connection"] = BusinessConnection.de_json( + data.get("business_connection"), bot + ) + data["business_message"] = Message.de_json(data.get("business_message"), bot) + data["edited_business_message"] = Message.de_json(data.get("edited_business_message"), bot) + data["deleted_business_messages"] = BusinessMessagesDeleted.de_json( + data.get("deleted_business_messages"), bot + ) return super().de_json(data=data, bot=bot) diff --git a/telegram/constants.py b/telegram/constants.py index 378862ff152..94a7f685cc2 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -2514,6 +2514,26 @@ class UpdateType(StringEnum): .. versionadded:: 20.8 """ + BUSINESS_CONNECTION = "business_connection" + """:obj:`str`: Updates with :attr:`telegram.Update.business_connection`. + + .. versionadded:: NEXT.VERSION + """ + BUSINESS_MESSAGE = "business_message" + """:obj:`str`: Updates with :attr:`telegram.Update.business_message`. + + .. versionadded:: NEXT.VERSION + """ + EDITED_BUSINESS_MESSAGE = "edited_business_message" + """:obj:`str`: Updates with :attr:`telegram.Update.edited_business_message`. + + .. versionadded:: NEXT.VERSION + """ + DELETED_BUSINESS_MESSAGES = "deleted_business_messages" + """:obj:`str`: Updates with :attr:`telegram.Update.deleted_business_messages`. + + .. versionadded:: NEXT.VERSION + """ class InvoiceLimit(IntEnum): diff --git a/tests/test_bot.py b/tests/test_bot.py index 853d9c305a5..fda1ae559bb 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2881,6 +2881,11 @@ async def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot, use_ip assert info.ip_address is None assert info.has_custom_certificate is False + async def test_get_business_connection(self, bot): + # TODO: Get a business connection and test this properly + pass + # assert await bot.get_business_connection("TEST") is None + async def test_leave_chat(self, bot): with pytest.raises(BadRequest, match="Chat not found"): await bot.leave_chat(-123456) diff --git a/tests/test_business.py b/tests/test_business.py new file mode 100644 index 00000000000..5514847a753 --- /dev/null +++ b/tests/test_business.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from datetime import datetime + +import pytest + +from telegram import BusinessConnection, BusinessMessagesDeleted, Chat, User +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + + +class TestBusinessBase: + id_ = "123" + user = User(123, "test_user", False) + user_chat_id = 123 + date = datetime.now(tz=UTC).replace(microsecond=0) + can_reply = True + is_enabled = True + message_ids = (123, 321) + business_connection_id = "123" + chat = Chat(123, "test_chat") + + +@pytest.fixture(scope="module") +def business_connection(): + return BusinessConnection( + TestBusinessBase.id_, + TestBusinessBase.user, + TestBusinessBase.user_chat_id, + TestBusinessBase.date, + TestBusinessBase.can_reply, + TestBusinessBase.is_enabled, + ) + + +@pytest.fixture(scope="module") +def business_messages_deleted(): + return BusinessMessagesDeleted( + TestBusinessBase.business_connection_id, + TestBusinessBase.chat, + TestBusinessBase.message_ids, + ) + + +class TestBusinessConnectionWithoutRequest(TestBusinessBase): + def test_slots(self, business_connection): + bc = business_connection + for attr in bc.__slots__: + assert getattr(bc, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bc)) == len(set(mro_slots(bc))), "duplicate slot" + + def test_de_json(self): + json_dict = { + "id": self.id_, + "user": self.user.to_dict(), + "user_chat_id": self.user_chat_id, + "date": to_timestamp(self.date), + "can_reply": self.can_reply, + "is_enabled": self.is_enabled, + } + bc = BusinessConnection.de_json(json_dict, None) + assert bc.id == self.id_ + assert bc.user == self.user + assert bc.user_chat_id == self.user_chat_id + assert bc.date == self.date + assert bc.can_reply == self.can_reply + assert bc.is_enabled == self.is_enabled + assert bc.api_kwargs == {} + assert isinstance(bc, BusinessConnection) + + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "id": self.id_, + "user": self.user.to_dict(), + "user_chat_id": self.user_chat_id, + "date": to_timestamp(self.date), + "can_reply": self.can_reply, + "is_enabled": self.is_enabled, + } + chat_bot = BusinessConnection.de_json(json_dict, bot) + chat_bot_raw = BusinessConnection.de_json(json_dict, raw_bot) + chat_bot_tz = BusinessConnection.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing tzinfo objects is not reliable + date_offset = chat_bot_tz.date.utcoffset() + date_offset_tz = tz_bot.defaults.tzinfo.utcoffset(chat_bot_tz.date.replace(tzinfo=None)) + + assert chat_bot.date.tzinfo == UTC + assert chat_bot_raw.date.tzinfo == UTC + assert date_offset_tz == date_offset + + def test_to_dict(self, business_connection): + bc_dict = business_connection.to_dict() + assert isinstance(bc_dict, dict) + assert bc_dict["id"] == self.id_ + assert bc_dict["user"] == self.user.to_dict() + assert bc_dict["user_chat_id"] == self.user_chat_id + assert bc_dict["date"] == to_timestamp(self.date) + assert bc_dict["can_reply"] == self.can_reply + assert bc_dict["is_enabled"] == self.is_enabled + + def test_equality(self): + bc1 = BusinessConnection( + self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + ) + bc2 = BusinessConnection( + self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + ) + bc3 = BusinessConnection( + "321", self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + ) + + assert bc1 == bc2 + assert hash(bc1) == hash(bc2) + + assert bc1 != bc3 + assert hash(bc1) != hash(bc3) + + +class TestBusinessMessagesDeleted(TestBusinessBase): + def test_slots(self, business_messages_deleted): + bmd = business_messages_deleted + for attr in bmd.__slots__: + assert getattr(bmd, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bmd)) == len(set(mro_slots(bmd))), "duplicate slot" + + def test_to_dict(self, business_messages_deleted): + bmd_dict = business_messages_deleted.to_dict() + assert isinstance(bmd_dict, dict) + assert bmd_dict["message_ids"] == list(self.message_ids) + assert bmd_dict["business_connection_id"] == self.business_connection_id + assert bmd_dict["chat"] == self.chat.to_dict() + + def test_de_json(self): + json_dict = { + "business_connection_id": self.business_connection_id, + "chat": self.chat.to_dict(), + "message_ids": self.message_ids, + } + bmd = BusinessMessagesDeleted.de_json(json_dict, None) + assert bmd.business_connection_id == self.business_connection_id + assert bmd.chat == self.chat + assert bmd.message_ids == self.message_ids + assert bmd.api_kwargs == {} + assert isinstance(bmd, BusinessMessagesDeleted) + + def test_equality(self): + bmd1 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids) + bmd2 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids) + bmd3 = BusinessMessagesDeleted("1", Chat(4, "random"), [321, 123]) + + assert bmd1 == bmd2 + assert hash(bmd1) == hash(bmd2) + + assert bmd1 != bmd3 + assert hash(bmd1) != hash(bmd3) diff --git a/tests/test_update.py b/tests/test_update.py index e46608f8a46..ba6c39ae1bb 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -23,6 +23,8 @@ import pytest from telegram import ( + BusinessConnection, + BusinessMessagesDeleted, CallbackQuery, Chat, ChatBoost, @@ -119,6 +121,21 @@ reactions=(ReactionCount(ReactionTypeEmoji("👍"), 1),), ) +business_connection = BusinessConnection( + "1", + User(1, "name", False), + 1, + from_timestamp(int(time.time())), + True, + True, +) + +deleted_business_messages = BusinessMessagesDeleted( + "1", + Chat(1, ""), + (1, 2), +) + params = [ {"message": message}, @@ -150,6 +167,8 @@ {"removed_chat_boost": removed_chat_boost}, {"message_reaction": message_reaction}, {"message_reaction_count": message_reaction_count}, + {"business_connection": business_connection}, + {"deleted_business_messages": deleted_business_messages}, # Must be last to conform with `ids` below! {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, ] @@ -173,6 +192,8 @@ "removed_chat_boost", "message_reaction", "message_reaction_count", + "business_connection", + "deleted_business_messages", ) ids = (*all_types, "callback_query_without_message") @@ -257,6 +278,7 @@ def test_effective_chat(self, update): or update.pre_checkout_query is not None or update.poll is not None or update.poll_answer is not None + or update.business_connection is not None ): assert chat.id == 1 else: @@ -272,6 +294,7 @@ def test_effective_user(self, update): or update.chat_boost is not None or update.removed_chat_boost is not None or update.message_reaction_count is not None + or update.deleted_business_messages is not None ): assert user.id == 1 else: @@ -297,6 +320,7 @@ def test_effective_sender_non_anonymous(self, update): or update.chat_boost is not None or update.removed_chat_boost is not None or update.message_reaction_count is not None + or update.deleted_business_messages is not None ): if update.channel_post or update.edited_channel_post: assert isinstance(sender, Chat) @@ -329,6 +353,7 @@ def test_effective_sender_anonymous(self, update): or update.chat_boost is not None or update.removed_chat_boost is not None or update.message_reaction_count is not None + or update.deleted_business_messages is not None ): if ( update.message @@ -365,6 +390,8 @@ def test_effective_message(self, update): or update.removed_chat_boost is not None or update.message_reaction is not None or update.message_reaction_count is not None + or update.deleted_business_messages is not None + or update.business_connection is not None ): assert eff_message.message_id == message.message_id else: From 6bb22f41e70de2cb9e8d72cb4db0c5085e329c0c Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:49:58 -0400 Subject: [PATCH 07/30] forgot to add method to extbot --- telegram/ext/_extbot.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 3ba1430490c..6b4d3f89af2 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -48,6 +48,7 @@ BotDescription, BotName, BotShortDescription, + BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -4038,6 +4039,26 @@ async def set_message_reaction( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_business_connection( + self, + business_connection_id: str, + *, + 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: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> BusinessConnection: + return await super().get_business_connection( + business_connection_id=business_connection_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 @@ -4157,3 +4178,4 @@ async def set_message_reaction( unpinAllGeneralForumTopicMessages = unpin_all_general_forum_topic_messages getUserChatBoosts = get_user_chat_boosts setMessageReaction = set_message_reaction + getBusinessConnection = get_business_connection From 1b33dfaaa364004044834726ba5e3cf174f0fc00 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 1 Apr 2024 03:59:30 -0400 Subject: [PATCH 08/30] Add new handlers and filters --- ...telegram.ext.businessconnectionhandler.rst | 6 + .../telegram.ext.businessmessagesdeleted.rst | 6 + docs/source/telegram.ext.handlers-tree.rst | 2 + telegram/ext/__init__.py | 4 + .../_handlers/businessconnectionhandler.py | 97 ++++++++++ .../businessmessagesdeletedhandler.py | 97 ++++++++++ telegram/ext/filters.py | 72 +++++++- tests/ext/test_businessconnectionhandler.py | 173 ++++++++++++++++++ .../test_businessmessagesdeletedhandler.py | 170 +++++++++++++++++ tests/ext/test_filters.py | 38 ++++ 10 files changed, 657 insertions(+), 8 deletions(-) create mode 100644 docs/source/telegram.ext.businessconnectionhandler.rst create mode 100644 docs/source/telegram.ext.businessmessagesdeleted.rst create mode 100644 telegram/ext/_handlers/businessconnectionhandler.py create mode 100644 telegram/ext/_handlers/businessmessagesdeletedhandler.py create mode 100644 tests/ext/test_businessconnectionhandler.py create mode 100644 tests/ext/test_businessmessagesdeletedhandler.py diff --git a/docs/source/telegram.ext.businessconnectionhandler.rst b/docs/source/telegram.ext.businessconnectionhandler.rst new file mode 100644 index 00000000000..0b0509dff2f --- /dev/null +++ b/docs/source/telegram.ext.businessconnectionhandler.rst @@ -0,0 +1,6 @@ +BusinessConnectionHandler +========================= + +.. autoclass:: telegram.ext.BusinessConnectionHandler + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.ext.businessmessagesdeleted.rst b/docs/source/telegram.ext.businessmessagesdeleted.rst new file mode 100644 index 00000000000..840f19325a0 --- /dev/null +++ b/docs/source/telegram.ext.businessmessagesdeleted.rst @@ -0,0 +1,6 @@ +BusinessMessagesDeletedHandler +============================== + +.. autoclass:: telegram.ext.BusinessMessagesDeletedHandler + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.ext.handlers-tree.rst b/docs/source/telegram.ext.handlers-tree.rst index e5df80b2cc6..5ee629eaaad 100644 --- a/docs/source/telegram.ext.handlers-tree.rst +++ b/docs/source/telegram.ext.handlers-tree.rst @@ -5,6 +5,8 @@ Handlers :titlesonly: telegram.ext.basehandler + telegram.ext.businessconnectionhandler + telegram.ext.businessmessagesdeleted telegram.ext.callbackqueryhandler telegram.ext.chatboosthandler telegram.ext.chatjoinrequesthandler diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index d1101bcf21c..82dbd1c19ad 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -27,6 +27,8 @@ "BasePersistence", "BaseRateLimiter", "BaseUpdateProcessor", + "BusinessConnectionHandler", + "BusinessMessagesDeletedHandler", "CallbackContext", "CallbackDataCache", "CallbackQueryHandler", @@ -75,6 +77,8 @@ from ._dictpersistence import DictPersistence from ._extbot import ExtBot from ._handlers.basehandler import BaseHandler +from ._handlers.businessconnectionhandler import BusinessConnectionHandler +from ._handlers.businessmessagesdeletedhandler import BusinessMessagesDeletedHandler from ._handlers.callbackqueryhandler import CallbackQueryHandler from ._handlers.chatboosthandler import ChatBoostHandler from ._handlers.chatjoinrequesthandler import ChatJoinRequestHandler diff --git a/telegram/ext/_handlers/businessconnectionhandler.py b/telegram/ext/_handlers/businessconnectionhandler.py new file mode 100644 index 00000000000..0d7833ffae3 --- /dev/null +++ b/telegram/ext/_handlers/businessconnectionhandler.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the BusinessConnectionHandler class.""" +from typing import Optional, TypeVar + +from telegram import Update +from telegram._utils.defaultvalue import DEFAULT_TRUE +from telegram._utils.types import SCT, DVType +from telegram.ext._handlers.basehandler import BaseHandler +from telegram.ext._utils._update_parsing import parse_chat_id, parse_username +from telegram.ext._utils.types import CCT, HandlerCallback + +RT = TypeVar("RT") + + +class BusinessConnectionHandler(BaseHandler[Update, CCT]): + """Handler class to handle Telegram + :attr:`Business Connections `. + + .. versionadded:: NEXT.VERSION + + Args: + callback (:term:`coroutine function`): The callback function for this handler. Will be + called when :meth:`check_update` has determined that an update should be processed by + this handler. Callback signature:: + + async def callback(update: Update, context: CallbackContext) + user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only + those which are asking to join the specified user ID(s). + + username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only + those which are asking to join the specified username(s). + + block (:obj:`bool`, optional): Determines whether the return value of the callback should + be awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. + + .. seealso:: :wiki:`Concurrency` + Attributes: + callback (:term:`coroutine function`): The callback function for this handler. + block (:obj:`bool`): Determines whether the return value of the callback should be + awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. + """ + + __slots__ = ( + "_user_ids", + "_usernames", + ) + + def __init__( + self, + callback: HandlerCallback[Update, CCT, RT], + user_id: Optional[SCT[int]] = None, + username: Optional[SCT[str]] = None, + block: DVType[bool] = DEFAULT_TRUE, + ): + super().__init__(callback, block=block) + + self._user_ids = parse_chat_id(user_id) + self._usernames = parse_username(username) + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handler's :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + if isinstance(update, Update) and update.business_connection: + if not self._user_ids and not self._usernames: + return True + if update.business_connection.user.id in self._user_ids: + return True + if update.business_connection.user.username in self._usernames: + return True + return False + return False diff --git a/telegram/ext/_handlers/businessmessagesdeletedhandler.py b/telegram/ext/_handlers/businessmessagesdeletedhandler.py new file mode 100644 index 00000000000..10785afec37 --- /dev/null +++ b/telegram/ext/_handlers/businessmessagesdeletedhandler.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the BusinessMessagesDeletedHandler class.""" +from typing import Optional, TypeVar + +from telegram import Update +from telegram._utils.defaultvalue import DEFAULT_TRUE +from telegram._utils.types import SCT, DVType +from telegram.ext._handlers.basehandler import BaseHandler +from telegram.ext._utils._update_parsing import parse_chat_id, parse_username +from telegram.ext._utils.types import CCT, HandlerCallback + +RT = TypeVar("RT") + + +class BusinessMessagesDeletedHandler(BaseHandler[Update, CCT]): + """Handler class to handle + :attr:`deleted Telegram Business messages `. + + .. versionadded:: NEXT.VERSION + + Args: + callback (:term:`coroutine function`): The callback function for this handler. Will be + called when :meth:`check_update` has determined that an update should be processed by + this handler. Callback signature:: + + async def callback(update: Update, context: CallbackContext) + chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only + those which are asking to join the specified chat ID(s). + + username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only + those which are asking to join the specified username(s). + + block (:obj:`bool`, optional): Determines whether the return value of the callback should + be awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. + + .. seealso:: :wiki:`Concurrency` + Attributes: + callback (:term:`coroutine function`): The callback function for this handler. + block (:obj:`bool`): Determines whether the return value of the callback should be + awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. + """ + + __slots__ = ( + "_chat_ids", + "_usernames", + ) + + def __init__( + self, + callback: HandlerCallback[Update, CCT, RT], + chat_id: Optional[SCT[int]] = None, + username: Optional[SCT[str]] = None, + block: DVType[bool] = DEFAULT_TRUE, + ): + super().__init__(callback, block=block) + + self._chat_ids = parse_chat_id(chat_id) + self._usernames = parse_username(username) + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handler's :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + if isinstance(update, Update) and update.deleted_business_messages: + if not self._chat_ids and not self._usernames: + return True + if update.deleted_business_messages.chat.id in self._chat_ids: + return True + if update.deleted_business_messages.chat.username in self._usernames: + return True + return False + return False diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index aad3ad95dc6..d4c65b45247 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -278,14 +278,17 @@ def check_update(self, update: Update) -> Optional[Union[bool, FilterDataDict]]: Returns: :obj:`bool`: :obj:`True` if the update contains one of :attr:`~telegram.Update.channel_post`, :attr:`~telegram.Update.message`, - :attr:`~telegram.Update.edited_channel_post` or - :attr:`~telegram.Update.edited_message`, :obj:`False` otherwise. + :attr:`~telegram.Update.edited_channel_post`, + :attr:`~telegram.Update.edited_message`, :attr:`telegram.Update.business_message`, + :attr:`telegram.Update.edited_business_message`, or :obj:`False` otherwise. """ if ( # Only message updates should be handled. - update.channel_post + update.channel_post # pylint: disable=too-many-boolean-expressions or update.message or update.edited_channel_post or update.edited_message + or update.business_message + or update.edited_business_message ): return True return False @@ -2488,13 +2491,21 @@ class _Edited(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: - return update.edited_message is not None or update.edited_channel_post is not None + return ( + update.edited_message is not None + or update.edited_channel_post is not None + or update.edited_business_message is not None + ) EDITED = _Edited(name="filters.UpdateType.EDITED") - """Updates with either :attr:`telegram.Update.edited_message` or - :attr:`telegram.Update.edited_channel_post`. + """Updates with :attr:`telegram.Update.edited_message`, + :attr:`telegram.Update.edited_channel_post`, or + :attr:`telegram.Update.edited_business_message`. .. versionadded:: 20.0 + + .. versionchanged:: NEXT.VERSION + Added :attr:`telegram.Update.edited_business_message` to the filter. """ class _EditedChannelPost(UpdateFilter): @@ -2532,7 +2543,48 @@ def filter(self, update: Update) -> bool: MESSAGES = _Messages(name="filters.UpdateType.MESSAGES") """Updates with either :attr:`telegram.Update.message` or - :attr:`telegram.Update.edited_message`.""" + :attr:`telegram.Update.edited_message`. + """ + + class _BusinessMessage(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return update.business_message is not None + + BUSINESS_MESSAGE = _BusinessMessage(name="filters.UpdateType.BUSINESS_MESSAGE") + """Updates with :attr:`telegram.Update.business_message`. + + .. versionadded:: NEXT.VERSION""" + + class _EditedBusinessMessage(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return update.edited_business_message is not None + + EDITED_BUSINESS_MESSAGE = _EditedBusinessMessage( + name="filters.UpdateType.EDITED_BUSINESS_MESSAGE" + ) + """Updates with :attr:`telegram.Update.edited_business_message`. + + .. versionadded:: NEXT.VERSION + """ + + class _BusinessMessages(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return ( + update.business_message is not None or update.edited_business_message is not None + ) + + BUSINESS_MESSAGES = _BusinessMessages(name="filters.UpdateType.BUSINESS_MESSAGES") + """Updates with either :attr:`telegram.Update.business_message` or + :attr:`telegram.Update.edited_business_message`. + + .. versionadded:: NEXT.VERSION + """ class User(_ChatUserBaseFilter): @@ -2677,6 +2729,8 @@ class ViaBot(_ChatUserBaseFilter): Examples: ``MessageHandler(filters.ViaBot(1234), callback_method)`` + .. seealso:: :attr:`filters.VIA_BOT` + Args: bot_id(:obj:`int` | Collection[:obj:`int`], optional): Which bot ID(s) to allow through. @@ -2758,7 +2812,9 @@ def filter(self, message: Message) -> bool: VIA_BOT = _ViaBot(name="filters.VIA_BOT") -"""This filter filters for message that were sent via *any* bot.""" +"""This filter filters for message that were sent via *any* bot. + +.. seealso:: :attr:`filters.ViaBot`""" class _Video(MessageFilter): diff --git a/tests/ext/test_businessconnectionhandler.py b/tests/ext/test_businessconnectionhandler.py new file mode 100644 index 00000000000..c8d741332a4 --- /dev/null +++ b/tests/ext/test_businessconnectionhandler.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import asyncio +import datetime + +import pytest + +from telegram import ( + Bot, + BusinessConnection, + CallbackQuery, + Chat, + ChosenInlineResult, + Message, + PreCheckoutQuery, + ShippingQuery, + Update, + User, +) +from telegram._utils.datetime import UTC +from telegram.ext import BusinessConnectionHandler, CallbackContext, JobQueue +from tests.auxil.slots import mro_slots + +message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") + +params = [ + {"message": message}, + {"edited_message": message}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, + {"channel_post": message}, + {"edited_channel_post": message}, + {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, + {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, + {"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, +] + +ids = ( + "message", + "edited_message", + "callback_query", + "channel_post", + "edited_channel_post", + "chosen_inline_result", + "shipping_query", + "pre_checkout_query", + "callback_query_without_message", +) + + +@pytest.fixture(scope="class", params=params, ids=ids) +def false_update(request): + return Update(update_id=2, **request.param) + + +@pytest.fixture(scope="class") +def time(): + return datetime.datetime.now(tz=UTC) + + +@pytest.fixture(scope="class") +def business_connection(bot): + bc = BusinessConnection( + id="1", + user_chat_id=1, + user=User(1, "name", username="user_a", is_bot=False), + date=datetime.datetime.now(tz=UTC), + can_reply=True, + is_enabled=True, + ) + bc.set_bot(bot) + return bc + + +@pytest.fixture() +def business_connection_update(bot, business_connection): + return Update(0, business_connection=business_connection) + + +class TestBusinessConnectionHandler: + test_flag = False + + def test_slot_behaviour(self): + action = BusinessConnectionHandler(self.callback) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + @pytest.fixture(autouse=True) + def _reset(self): + self.test_flag = False + + async def callback(self, update, context): + self.test_flag = ( + isinstance(context, CallbackContext) + and isinstance(context.bot, Bot) + and isinstance(update, Update) + and isinstance(context.update_queue, asyncio.Queue) + and isinstance(context.job_queue, JobQueue) + and isinstance(context.user_data, dict) + and isinstance(context.bot_data, dict) + and isinstance( + update.business_connection, + BusinessConnection, + ) + ) + + def test_with_user_id(self, business_connection_update): + handler = BusinessConnectionHandler(self.callback, user_id=1) + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, user_id=[1]) + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, user_id=2, username="@user_a") + assert handler.check_update(business_connection_update) + + handler = BusinessConnectionHandler(self.callback, user_id=2) + assert not handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, user_id=[2]) + assert not handler.check_update(business_connection_update) + + def test_with_username(self, business_connection_update): + handler = BusinessConnectionHandler(self.callback, username="user_a") + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username="@user_a") + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username=["user_a"]) + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username=["@user_a"]) + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, user_id=1, username="@user_b") + assert handler.check_update(business_connection_update) + + handler = BusinessConnectionHandler(self.callback, username="user_b") + assert not handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username="@user_b") + assert not handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username=["user_b"]) + assert not handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username=["@user_b"]) + assert not handler.check_update(business_connection_update) + + business_connection_update.business_connection.user._unfreeze() + business_connection_update.business_connection.user.username = None + assert not handler.check_update(business_connection_update) + + def test_other_update_types(self, false_update): + handler = BusinessConnectionHandler(self.callback) + assert not handler.check_update(false_update) + assert not handler.check_update(True) + + async def test_context(self, app, business_connection_update): + handler = BusinessConnectionHandler(callback=self.callback) + app.add_handler(handler) + + async with app: + await app.process_update(business_connection_update) + assert self.test_flag diff --git a/tests/ext/test_businessmessagesdeletedhandler.py b/tests/ext/test_businessmessagesdeletedhandler.py new file mode 100644 index 00000000000..a15a0a0c2b4 --- /dev/null +++ b/tests/ext/test_businessmessagesdeletedhandler.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import asyncio +import datetime + +import pytest + +from telegram import ( + Bot, + BusinessMessagesDeleted, + CallbackQuery, + Chat, + ChosenInlineResult, + Message, + PreCheckoutQuery, + ShippingQuery, + Update, + User, +) +from telegram._utils.datetime import UTC +from telegram.ext import BusinessMessagesDeletedHandler, CallbackContext, JobQueue +from tests.auxil.slots import mro_slots + +message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") + +params = [ + {"message": message}, + {"edited_message": message}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, + {"channel_post": message}, + {"edited_channel_post": message}, + {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, + {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, + {"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, +] + +ids = ( + "message", + "edited_message", + "callback_query", + "channel_post", + "edited_channel_post", + "chosen_inline_result", + "shipping_query", + "pre_checkout_query", + "callback_query_without_message", +) + + +@pytest.fixture(scope="class", params=params, ids=ids) +def false_update(request): + return Update(update_id=2, **request.param) + + +@pytest.fixture(scope="class") +def time(): + return datetime.datetime.now(tz=UTC) + + +@pytest.fixture(scope="class") +def business_messages_deleted(bot): + bmd = BusinessMessagesDeleted( + business_connection_id="1", + chat=Chat(1, Chat.PRIVATE, username="user_a"), + message_ids=[1, 2, 3], + ) + bmd.set_bot(bot) + return bmd + + +@pytest.fixture() +def business_messages_deleted_update(bot, business_messages_deleted): + return Update(0, deleted_business_messages=business_messages_deleted) + + +class TestBusinessMessagesDeletedHandler: + test_flag = False + + def test_slot_behaviour(self): + action = BusinessMessagesDeletedHandler(self.callback) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + @pytest.fixture(autouse=True) + def _reset(self): + self.test_flag = False + + async def callback(self, update, context): + self.test_flag = ( + isinstance(context, CallbackContext) + and isinstance(context.bot, Bot) + and isinstance(update, Update) + and isinstance(context.update_queue, asyncio.Queue) + and isinstance(context.job_queue, JobQueue) + and isinstance(context.chat_data, dict) + and isinstance(context.bot_data, dict) + and isinstance( + update.deleted_business_messages, + BusinessMessagesDeleted, + ) + ) + + def test_with_chat_id(self, business_messages_deleted_update): + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=1) + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=[1]) + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=2, username="@user_a") + assert handler.check_update(business_messages_deleted_update) + + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=2) + assert not handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=[2]) + assert not handler.check_update(business_messages_deleted_update) + + def test_with_username(self, business_messages_deleted_update): + handler = BusinessMessagesDeletedHandler(self.callback, username="user_a") + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username="@user_a") + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username=["user_a"]) + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username=["@user_a"]) + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=1, username="@user_b") + assert handler.check_update(business_messages_deleted_update) + + handler = BusinessMessagesDeletedHandler(self.callback, username="user_b") + assert not handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username="@user_b") + assert not handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username=["user_b"]) + assert not handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username=["@user_b"]) + assert not handler.check_update(business_messages_deleted_update) + + business_messages_deleted_update.deleted_business_messages.chat._unfreeze() + business_messages_deleted_update.deleted_business_messages.chat.username = None + assert not handler.check_update(business_messages_deleted_update) + + def test_other_update_types(self, false_update): + handler = BusinessMessagesDeletedHandler(self.callback) + assert not handler.check_update(false_update) + assert not handler.check_update(True) + + async def test_context(self, app, business_messages_deleted_update): + handler = BusinessMessagesDeletedHandler(callback=self.callback) + app.add_handler(handler) + + async with app: + await app.process_update(business_messages_deleted_update) + assert self.test_flag diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index bcd1980914e..de9b30cb755 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -2343,6 +2343,9 @@ def test_update_type_message(self, update): assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) assert not filters.UpdateType.EDITED.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_edited_message(self, update): update.edited_message, update.message = update.message, update.edited_message @@ -2353,6 +2356,9 @@ def test_update_type_edited_message(self, update): assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) assert filters.UpdateType.EDITED.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_channel_post(self, update): update.channel_post, update.message = update.message, update.edited_message @@ -2363,6 +2369,9 @@ def test_update_type_channel_post(self, update): assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert filters.UpdateType.CHANNEL_POSTS.check_update(update) assert not filters.UpdateType.EDITED.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_edited_channel_post(self, update): update.edited_channel_post, update.message = update.message, update.edited_message @@ -2373,6 +2382,35 @@ def test_update_type_edited_channel_post(self, update): assert filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert filters.UpdateType.CHANNEL_POSTS.check_update(update) assert filters.UpdateType.EDITED.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) + + def test_update_type_business_message(self, update): + update.business_message, update.message = update.message, update.edited_message + assert not filters.UpdateType.MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) + assert not filters.UpdateType.MESSAGES.check_update(update) + assert not filters.UpdateType.CHANNEL_POST.check_update(update) + assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) + assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) + assert not filters.UpdateType.EDITED.check_update(update) + assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) + + def test_update_type_edited_business_message(self, update): + update.edited_business_message, update.message = update.message, update.edited_message + assert not filters.UpdateType.MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) + assert not filters.UpdateType.MESSAGES.check_update(update) + assert not filters.UpdateType.CHANNEL_POST.check_update(update) + assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) + assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) + assert filters.UpdateType.EDITED.check_update(update) + assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_merged_short_circuit_and(self, update, base_class): update.message.text = "/test" From dc3c1b983e7862d826e8411dedd969ccfbd94023 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 1 Apr 2024 04:15:50 -0400 Subject: [PATCH 09/30] fix doc build and improve type completeness --- .../telegram.ext.businessmessagesdeleted.rst | 6 ------ docs/source/telegram.ext.handlers-tree.rst | 2 +- telegram/_business.py | 18 +++++++++--------- telegram/ext/filters.py | 4 ++-- 4 files changed, 12 insertions(+), 18 deletions(-) delete mode 100644 docs/source/telegram.ext.businessmessagesdeleted.rst diff --git a/docs/source/telegram.ext.businessmessagesdeleted.rst b/docs/source/telegram.ext.businessmessagesdeleted.rst deleted file mode 100644 index 840f19325a0..00000000000 --- a/docs/source/telegram.ext.businessmessagesdeleted.rst +++ /dev/null @@ -1,6 +0,0 @@ -BusinessMessagesDeletedHandler -============================== - -.. autoclass:: telegram.ext.BusinessMessagesDeletedHandler - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.ext.handlers-tree.rst b/docs/source/telegram.ext.handlers-tree.rst index 5ee629eaaad..6749cacb9dd 100644 --- a/docs/source/telegram.ext.handlers-tree.rst +++ b/docs/source/telegram.ext.handlers-tree.rst @@ -6,7 +6,7 @@ Handlers telegram.ext.basehandler telegram.ext.businessconnectionhandler - telegram.ext.businessmessagesdeleted + telegram.ext.businessmessagesdeletedhandler telegram.ext.callbackqueryhandler telegram.ext.chatboosthandler telegram.ext.chatjoinrequesthandler diff --git a/telegram/_business.py b/telegram/_business.py index e7ec28855b9..953d5d967a6 100644 --- a/telegram/_business.py +++ b/telegram/_business.py @@ -85,12 +85,12 @@ def __init__( api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) - self.id = id - self.user = user - self.user_chat_id = user_chat_id - self.date = date - self.can_reply = can_reply - self.is_enabled = is_enabled + self.id: str = id + self.user: User = user + self.user_chat_id: int = user_chat_id + self.date: datetime = date + self.can_reply: bool = can_reply + self.is_enabled: bool = is_enabled self._id_attrs = ( self.id, @@ -160,9 +160,9 @@ def __init__( api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) - self.business_connection_id = business_connection_id - self.chat = chat - self.message_ids = parse_sequence_arg(message_ids) + self.business_connection_id: str = business_connection_id + self.chat: Chat = chat + self.message_ids: Sequence[int] = parse_sequence_arg(message_ids) self._id_attrs = ( self.business_connection_id, diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index d4c65b45247..5eb700aa89c 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -2729,7 +2729,7 @@ class ViaBot(_ChatUserBaseFilter): Examples: ``MessageHandler(filters.ViaBot(1234), callback_method)`` - .. seealso:: :attr:`filters.VIA_BOT` + .. seealso:: :attr:`~telegram.ext.filters.VIA_BOT` Args: bot_id(:obj:`int` | Collection[:obj:`int`], optional): Which bot ID(s) to @@ -2814,7 +2814,7 @@ def filter(self, message: Message) -> bool: VIA_BOT = _ViaBot(name="filters.VIA_BOT") """This filter filters for message that were sent via *any* bot. -.. seealso:: :attr:`filters.ViaBot`""" +.. seealso:: :class:`~telegram.ext.filters.ViaBot`""" class _Video(MessageFilter): From 3ff7a6896c46c4efcd43c03bfd4525f2bc7a037b Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:43:12 -0400 Subject: [PATCH 10/30] Apply Sticker method and pack changes --- ...ram.ext.businessmessagesdeletedhandler.rst | 6 +++ telegram/_bot.py | 35 ++++++++++++--- telegram/_files/inputsticker.py | 16 ++++++- telegram/_files/sticker.py | 18 ++------ telegram/constants.py | 3 ++ telegram/ext/_extbot.py | 4 +- tests/_files/test_inputsticker.py | 4 ++ tests/_files/test_sticker.py | 45 ++++++++++--------- 8 files changed, 88 insertions(+), 43 deletions(-) create mode 100644 docs/source/telegram.ext.businessmessagesdeletedhandler.rst diff --git a/docs/source/telegram.ext.businessmessagesdeletedhandler.rst b/docs/source/telegram.ext.businessmessagesdeletedhandler.rst new file mode 100644 index 00000000000..840f19325a0 --- /dev/null +++ b/docs/source/telegram.ext.businessmessagesdeletedhandler.rst @@ -0,0 +1,6 @@ +BusinessMessagesDeletedHandler +============================== + +.. autoclass:: telegram.ext.BusinessMessagesDeletedHandler + :members: + :show-inheritance: \ No newline at end of file diff --git a/telegram/_bot.py b/telegram/_bot.py index e66b433b81e..20444c81a1c 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -1717,8 +1717,8 @@ async def send_sticker( chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Sticker`): Sticker to send. - |fileinput| Video stickers can only be sent by a ``file_id``. Animated stickers - can't be sent via an HTTP URL. + |fileinput| Video stickers can only be sent by a ``file_id``. Video and animated + stickers can't be sent via an HTTP URL. Lastly you can pass an existing :class:`telegram.Sticker` object to send. @@ -6225,9 +6225,7 @@ async def add_sticker_to_set( """ Use this method to add a new sticker to a set created by the bot. The format of the added sticker must match the format of the other stickers in the set. Emoji sticker sets can have - up to :tg-const:`telegram.constants.StickerSetLimit.MAX_EMOJI_STICKERS` stickers. Animated - and video sticker sets can have up to - :tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_STICKERS` stickers. Static + up to :tg-const:`telegram.constants.StickerSetLimit.MAX_EMOJI_STICKERS` stickers. Other sticker sets can have up to :tg-const:`telegram.constants.StickerSetLimit.MAX_STATIC_STICKERS` stickers. @@ -6312,7 +6310,7 @@ async def create_new_sticker_set( name: str, title: str, stickers: Sequence["InputSticker"], - sticker_format: str, + sticker_format: Optional[str] = None, sticker_type: Optional[str] = None, needs_repainting: Optional[bool] = None, *, @@ -6365,6 +6363,9 @@ async def create_new_sticker_set( .. versionadded:: 20.2 + .. deprecated:: NEXT.VERSION + Use :paramref:`telegram.InputSticker.format` instead. + sticker_type (:obj:`str`, optional): Type of stickers in the set, pass :attr:`telegram.Sticker.REGULAR` or :attr:`telegram.Sticker.MASK`, or :attr:`telegram.Sticker.CUSTOM_EMOJI`. By default, a regular sticker set is created @@ -6384,6 +6385,14 @@ async def create_new_sticker_set( Raises: :class:`telegram.error.TelegramError` """ + if sticker_format is not None: + warn( + "The parameter 'sticker_format' is deprecated. Use the parameter" + " `InputSticker.format` in the parameter `stickers` instead.", + stacklevel=2, + category=PTBDeprecationWarning, + ) + data: JSONDict = { "user_id": user_id, "name": name, @@ -6477,6 +6486,7 @@ async def set_sticker_set_thumbnail( self, name: str, user_id: int, + format: str, # pylint: disable=redefined-builtin thumbnail: Optional[FileInput] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -6490,9 +6500,21 @@ async def set_sticker_set_thumbnail( .. versionadded:: 20.2 + .. versionchanged:: NEXT.VERSION + As per Bot API 7.2, the new argument :paramref:`format` will be required, and thus the + order of the arguments had to be changed. + Args: name (:obj:`str`): Sticker set name user_id (:obj:`int`): User identifier of created sticker set owner. + format (:obj:`str`): Format of the added sticker, must be one of + :tg-const:`telegram.constants.StickerFormat.STATIC` for a + ``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED` + for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a + WEBM video. + + .. versionadded:: NEXT.VERSION + thumbnail (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ optional): A **.WEBP** or **.PNG** image with the thumbnail, must be up to :tg-const:`telegram.constants.StickerSetLimit.MAX_STATIC_THUMBNAIL_SIZE` @@ -6525,6 +6547,7 @@ async def set_sticker_set_thumbnail( "name": name, "user_id": user_id, "thumbnail": self._parse_file_input(thumbnail) if thumbnail else None, + "format": format, } return await self._post( diff --git a/telegram/_files/inputsticker.py b/telegram/_files/inputsticker.py index bfcd89300a2..b5189fd4c32 100644 --- a/telegram/_files/inputsticker.py +++ b/telegram/_files/inputsticker.py @@ -52,6 +52,11 @@ class InputSticker(TelegramObject): :tg-const:`telegram.constants.StickerLimit.MAX_KEYWORD_LENGTH` characters. For ":tg-const:`telegram.constants.StickerType.REGULAR`" and ":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only. + format (:obj:`str`, optional): Format of the added sticker, must be one of + :tg-const:`telegram.constants.StickerFormat.STATIC` for a + ``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED` + for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM + video. Attributes: sticker (:obj:`str` | :class:`telegram.InputFile`): The added sticker. @@ -67,10 +72,15 @@ class InputSticker(TelegramObject): :tg-const:`telegram.constants.StickerLimit.MAX_KEYWORD_LENGTH` characters. For ":tg-const:`telegram.constants.StickerType.REGULAR`" and ":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only. - + ":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only. + format (:obj:`str`): Optional. Format of the added sticker, must be one of + :tg-const:`telegram.constants.StickerFormat.STATIC` for a + ``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED` + for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM + video. """ - __slots__ = ("emoji_list", "keywords", "mask_position", "sticker") + __slots__ = ("emoji_list", "format", "keywords", "mask_position", "sticker") def __init__( self, @@ -78,6 +88,7 @@ def __init__( emoji_list: Sequence[str], mask_position: Optional[MaskPosition] = None, keywords: Optional[Sequence[str]] = None, + format: Optional[str] = None, # pylint: disable=redefined-builtin *, api_kwargs: Optional[JSONDict] = None, ): @@ -93,5 +104,6 @@ def __init__( self.emoji_list: Tuple[str, ...] = parse_sequence_arg(emoji_list) self.mask_position: Optional[MaskPosition] = mask_position self.keywords: Tuple[str, ...] = parse_sequence_arg(keywords) + self.format: Optional[str] = format self._freeze() diff --git a/telegram/_files/sticker.py b/telegram/_files/sticker.py index cb7b5deac0b..f6fcc6049dc 100644 --- a/telegram/_files/sticker.py +++ b/telegram/_files/sticker.py @@ -227,16 +227,16 @@ class StickerSet(TelegramObject): .. versionchanged:: 20.0 The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` instead. + + .. versionchanged:: NEXT.VERSION + The parameter ``is_video`` and ``is_animated`` has been removed. + .. versionchanged:: 20.5 |removed_thumb_note| Args: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. - is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers. - is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. - - .. versionadded:: 13.11 stickers (Sequence[:class:`telegram.Sticker`]): List of all set stickers. .. versionchanged:: 20.0 @@ -255,10 +255,6 @@ class StickerSet(TelegramObject): Attributes: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. - is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers. - is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. - - .. versionadded:: 13.11 stickers (Tuple[:class:`telegram.Sticker`]): List of all set stickers. .. versionchanged:: 20.0 @@ -276,8 +272,6 @@ class StickerSet(TelegramObject): """ __slots__ = ( - "is_animated", - "is_video", "name", "sticker_type", "stickers", @@ -289,9 +283,7 @@ def __init__( self, name: str, title: str, - is_animated: bool, stickers: Sequence[Sticker], - is_video: bool, sticker_type: str, thumbnail: Optional[PhotoSize] = None, *, @@ -300,8 +292,6 @@ def __init__( super().__init__(api_kwargs=api_kwargs) self.name: str = name self.title: str = title - self.is_animated: bool = is_animated - self.is_video: bool = is_video self.stickers: Tuple[Sticker, ...] = parse_sequence_arg(stickers) self.sticker_type: str = sticker_type # Optional diff --git a/telegram/constants.py b/telegram/constants.py index 94a7f685cc2..c43ed6d130d 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -2322,6 +2322,9 @@ class StickerSetLimit(IntEnum): MAX_ANIMATED_STICKERS = 50 """:obj:`int`: Maximum number of stickers allowed in an animated or video sticker set, as given in :meth:`telegram.Bot.add_sticker_to_set`. + + .. deprecated:: NEXT.VERSION + The animated sticker limit is now 120, the same as :attr:`MAX_STATIC_STICKERS`. """ MAX_STATIC_STICKERS = 120 """:obj:`int`: Maximum number of stickers allowed in a static sticker set, as given in diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 6b4d3f89af2..7c36b895129 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -1185,7 +1185,7 @@ async def create_new_sticker_set( name: str, title: str, stickers: Sequence["InputSticker"], - sticker_format: str, + sticker_format: Optional[str] = None, sticker_type: Optional[str] = None, needs_repainting: Optional[bool] = None, *, @@ -3503,6 +3503,7 @@ async def set_sticker_set_thumbnail( self, name: str, user_id: int, + format: str, # pylint: disable=redefined-builtin thumbnail: Optional[FileInput] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3516,6 +3517,7 @@ async def set_sticker_set_thumbnail( name=name, user_id=user_id, thumbnail=thumbnail, + format=format, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, diff --git a/tests/_files/test_inputsticker.py b/tests/_files/test_inputsticker.py index c974853f268..c627cb56b10 100644 --- a/tests/_files/test_inputsticker.py +++ b/tests/_files/test_inputsticker.py @@ -33,6 +33,7 @@ def input_sticker(): emoji_list=TestInputStickerBase.emoji_list, mask_position=TestInputStickerBase.mask_position, keywords=TestInputStickerBase.keywords, + format=TestInputStickerBase.format, ) @@ -41,6 +42,7 @@ class TestInputStickerBase: emoji_list = ("👍", "👎") mask_position = MaskPosition("forehead", 0.5, 0.5, 0.5) keywords = ("thumbsup", "thumbsdown") + format = "static" class TestInputStickerNoRequest(TestInputStickerBase): @@ -56,6 +58,7 @@ def test_expected_values(self, input_sticker): assert input_sticker.emoji_list == self.emoji_list assert input_sticker.mask_position == self.mask_position assert input_sticker.keywords == self.keywords + assert input_sticker.format == self.format def test_attributes_tuple(self, input_sticker): assert isinstance(input_sticker.keywords, tuple) @@ -72,6 +75,7 @@ def test_to_dict(self, input_sticker): assert input_sticker_dict["emoji_list"] == list(input_sticker.emoji_list) assert input_sticker_dict["mask_position"] == input_sticker.mask_position.to_dict() assert input_sticker_dict["keywords"] == list(input_sticker.keywords) + assert input_sticker_dict["format"] == input_sticker.format def test_with_sticker_input_types(self, video_sticker_file): # noqa: F811 sticker = InputSticker(sticker=video_sticker_file, emoji_list=["👍"]) diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index 86a2da710c0..e8bc7c9a150 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -39,6 +39,7 @@ from telegram.constants import ParseMode, StickerFormat, StickerType from telegram.error import BadRequest, TelegramError from telegram.request import RequestData +from telegram.warnings import PTBDeprecationWarning from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, @@ -574,8 +575,6 @@ def sticker_set_thumb_file(): class TestStickerSetBase: title = "Test stickers" - is_animated = True - is_video = True stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True, Sticker.REGULAR)] name = "NOTAREALNAME" sticker_type = Sticker.REGULAR @@ -584,7 +583,7 @@ class TestStickerSetBase: class TestStickerSetWithoutRequest(TestStickerSetBase): def test_slot_behaviour(self): - inst = StickerSet("this", "is", True, self.stickers, True, "not") + inst = StickerSet("this", "is", self.stickers, "not") 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" @@ -594,8 +593,6 @@ def test_de_json(self, bot, sticker): json_dict = { "name": name, "title": self.title, - "is_animated": self.is_animated, - "is_video": self.is_video, "stickers": [x.to_dict() for x in self.stickers], "thumbnail": sticker.thumbnail.to_dict(), "sticker_type": self.sticker_type, @@ -605,8 +602,6 @@ def test_de_json(self, bot, sticker): assert sticker_set.name == name assert sticker_set.title == self.title - assert sticker_set.is_animated == self.is_animated - assert sticker_set.is_video == self.is_video assert sticker_set.stickers == tuple(self.stickers) assert sticker_set.thumbnail == sticker.thumbnail assert sticker_set.sticker_type == self.sticker_type @@ -618,8 +613,6 @@ def test_sticker_set_to_dict(self, sticker_set): assert isinstance(sticker_set_dict, dict) assert sticker_set_dict["name"] == sticker_set.name assert sticker_set_dict["title"] == sticker_set.title - assert sticker_set_dict["is_animated"] == sticker_set.is_animated - assert sticker_set_dict["is_video"] == sticker_set.is_video assert sticker_set_dict["stickers"][0] == sticker_set.stickers[0].to_dict() assert sticker_set_dict["thumbnail"] == sticker_set.thumbnail.to_dict() assert sticker_set_dict["sticker_type"] == sticker_set.sticker_type @@ -628,26 +621,20 @@ def test_equality(self): a = StickerSet( self.name, self.title, - self.is_animated, self.stickers, - self.is_video, self.sticker_type, ) b = StickerSet( self.name, self.title, - self.is_animated, self.stickers, - self.is_video, self.sticker_type, ) - c = StickerSet(self.name, "title", False, [], True, Sticker.CUSTOM_EMOJI) + c = StickerSet(self.name, "title", [], Sticker.CUSTOM_EMOJI) d = StickerSet( "blah", self.title, - self.is_animated, self.stickers, - self.is_video, self.sticker_type, ) e = Audio(self.name, "", 0, None, None) @@ -778,7 +765,7 @@ async def make_assertion(_, data, *args, **kwargs): test_flag = isinstance(data.get("thumbnail"), InputFile) monkeypatch.setattr(bot, "_post", make_assertion) - await bot.set_sticker_set_thumbnail("name", chat_id, thumbnail=file) + await bot.set_sticker_set_thumbnail("name", chat_id, thumbnail=file, format="static") assert test_flag finally: bot._local_mode = False @@ -794,6 +781,22 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(sticker.get_bot(), "get_file", make_assertion) assert await sticker.get_file() + async def test_create_new_sticker_set_format_arg_depr( + self, bot, chat_id, sticker_file, monkeypatch + ): + async def make_assertion(*_, **kwargs): + pass + + monkeypatch.setattr(bot, "_post", make_assertion) + with pytest.warns(PTBDeprecationWarning, match="`sticker_format` is deprecated"): + await bot.create_new_sticker_set( + chat_id, + "name", + "title", + stickers=sticker_file, + sticker_format="static", + ) + @pytest.mark.xdist_group("stickerset") class TestStickerSetWithRequest: @@ -943,7 +946,7 @@ async def test_bot_methods_2_webm(self, bot, video_sticker_set): async def test_bot_methods_3_png(self, bot, chat_id, sticker_set_thumb_file): await asyncio.sleep(1) assert await bot.set_sticker_set_thumbnail( - f"test_by_{bot.username}", chat_id, sticker_set_thumb_file + f"test_by_{bot.username}", chat_id, sticker_set_thumb_file, format="static" ) async def test_bot_methods_3_tgs( @@ -953,8 +956,10 @@ async def test_bot_methods_3_tgs( animated_test = f"animated_test_by_{bot.username}" file_id = animated_sticker_set.stickers[-1].file_id tasks = asyncio.gather( - bot.set_sticker_set_thumbnail(animated_test, chat_id, animated_sticker_file), - bot.set_sticker_set_thumbnail(animated_test, chat_id, file_id), + bot.set_sticker_set_thumbnail( + animated_test, chat_id, animated_sticker_file, format="animated" + ), + bot.set_sticker_set_thumbnail(animated_test, chat_id, file_id, format="animated"), ) assert all(await tasks) From e942ebfa4c2457fc15cb36cbc84d21980bb6b027 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 2 Apr 2024 03:43:39 -0400 Subject: [PATCH 11/30] fix tests again --- telegram/_bot.py | 2 +- tests/_files/test_sticker.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 20444c81a1c..9b0e55ae466 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -6387,7 +6387,7 @@ async def create_new_sticker_set( """ if sticker_format is not None: warn( - "The parameter 'sticker_format' is deprecated. Use the parameter" + "The parameter `sticker_format` is deprecated. Use the parameter" " `InputSticker.format` in the parameter `stickers` instead.", stacklevel=2, category=PTBDeprecationWarning, diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index e8bc7c9a150..72be6bafba7 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -946,7 +946,7 @@ async def test_bot_methods_2_webm(self, bot, video_sticker_set): async def test_bot_methods_3_png(self, bot, chat_id, sticker_set_thumb_file): await asyncio.sleep(1) assert await bot.set_sticker_set_thumbnail( - f"test_by_{bot.username}", chat_id, sticker_set_thumb_file, format="static" + f"test_by_{bot.username}", chat_id, format="static", thumbnail=sticker_set_thumb_file ) async def test_bot_methods_3_tgs( @@ -957,9 +957,12 @@ async def test_bot_methods_3_tgs( file_id = animated_sticker_set.stickers[-1].file_id tasks = asyncio.gather( bot.set_sticker_set_thumbnail( - animated_test, chat_id, animated_sticker_file, format="animated" + animated_test, + chat_id, + "animated", + thumbnail=animated_sticker_file, ), - bot.set_sticker_set_thumbnail(animated_test, chat_id, file_id, format="animated"), + bot.set_sticker_set_thumbnail(animated_test, chat_id, "animated", thumbnail=file_id), ) assert all(await tasks) From dcb471e31759ae9ca0489be46316c68b5287da5f Mon Sep 17 00:00:00 2001 From: Mahdyar Hasanpour Date: Tue, 2 Apr 2024 11:30:16 +0330 Subject: [PATCH 12/30] Feat: add the field `is_from_offline` to the class `Message` (#4189) --- telegram/_message.py | 9 +++++++++ tests/test_constants.py | 1 + tests/test_message.py | 2 ++ 3 files changed, 12 insertions(+) diff --git a/telegram/_message.py b/telegram/_message.py index 62848565a3d..4036511e210 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -302,6 +302,9 @@ class Message(MaybeInaccessibleMessage): forwarded. .. versionadded:: 13.9 + is_from_offline (:obj:`bool`, optional): :obj:`True`, if the message was sent + by an implicit action, for example, as an away or a greeting business message, + or as a scheduled message. media_group_id (:obj:`str`, optional): The unique identifier of a media message group this message belongs to. text (:obj:`str`, optional): For text messages, the actual UTF-8 text of the message, @@ -581,6 +584,9 @@ class Message(MaybeInaccessibleMessage): forwarded. .. versionadded:: 13.9 + is_from_offline (:obj:`bool`): Optional. :obj:`True`, if the message was sent + by an implicit action, for example, as an away or a greeting business message, + or as a scheduled message. media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this message belongs to. text (:obj:`str`): Optional. For text messages, the actual UTF-8 text of the message, @@ -890,6 +896,7 @@ class Message(MaybeInaccessibleMessage): "has_protected_content", "invoice", "is_automatic_forward", + "is_from_offline", "is_topic_message", "left_chat_member", "link_preview_options", @@ -1014,6 +1021,7 @@ def __init__( sender_boost_count: Optional[int] = None, business_connection_id: Optional[str] = None, sender_business_bot: Optional[User] = None, + is_from_offline: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1111,6 +1119,7 @@ def __init__( self.sender_boost_count: Optional[int] = sender_boost_count self.business_connection_id: Optional[str] = business_connection_id self.sender_business_bot: Optional[User] = sender_business_bot + self.is_from_offline: Optional[bool] = is_from_offline self._effective_attachment = DEFAULT_NONE diff --git a/tests/test_constants.py b/tests/test_constants.py index 5055f75795c..04dc6700564 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -176,6 +176,7 @@ def is_type_attribute(name: str) -> bool: # attribute is deprecated, no need to add it to MessageType "user_shared", "via_bot", + "is_from_offline", }: return False diff --git a/tests/test_message.py b/tests/test_message.py index dc2a0bfe15f..bd7dff92d6d 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -263,6 +263,7 @@ def message(bot): {"reply_to_story": Story(Chat(1, Chat.PRIVATE), 0)}, {"boost_added": ChatBoostAdded(100)}, {"sender_boost_count": 1}, + {"is_from_offline": True}, {"sender_business_bot": User(1, "BusinessBot", True)}, {"business_connection_id": "123456789"}, ], @@ -332,6 +333,7 @@ def message(bot): "sender_boost_count", "sender_business_bot", "business_connection_id", + "is_from_offline", ], ) def message_params(bot, request): From 6b6fcf5a0935bf9c8800db27bca9b77a4c81b92e Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:15:05 +0200 Subject: [PATCH 13/30] Revert "Temporarily Mark Tests with `get_sticker_set` as XFAIL due to API 7.2 Update (#4190)" This reverts commit 7331fff3fc849b18e0d103263b2d8ff6ecd89d1d. --- tests/_files/test_sticker.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index a62a1da78e9..72be6bafba7 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -472,7 +472,6 @@ async def test_send_sticker_default_protect_content(self, chat_id, sticker, defa assert protected.has_protected_content assert not unprotected.has_protected_content - @pytest.mark.xfail(reason="API 7.2 incompatibility, see #4181") async def test_premium_animation(self, bot): # testing animation sucks a bit since we can't create a premium sticker. What we can do is # get a sticker set which includes a premium sticker and check that specific one. @@ -490,7 +489,6 @@ async def test_premium_animation(self, bot): } assert premium_sticker.premium_animation.to_dict() == premium_sticker_dict - @pytest.mark.xfail(reason="API 7.2 incompatibility, see #4181") async def test_custom_emoji(self, bot): # testing custom emoji stickers is as much of an annoyance as the premium animation, see # in test_premium_animation @@ -529,7 +527,6 @@ async def test_error_send_empty_file_id(self, bot, chat_id): @pytest.fixture() async def sticker_set(bot): - pytest.xfail(reason="API 7.2 incompatibility, see #4181") ss = await bot.get_sticker_set(f"test_by_{bot.username}") if len(ss.stickers) > 100: try: @@ -544,7 +541,6 @@ async def sticker_set(bot): @pytest.fixture() async def animated_sticker_set(bot): - pytest.xfail(reason="API 7.2 incompatibility, see #4181") ss = await bot.get_sticker_set(f"animated_test_by_{bot.username}") if len(ss.stickers) > 100: try: @@ -803,7 +799,6 @@ async def make_assertion(*_, **kwargs): @pytest.mark.xdist_group("stickerset") -@pytest.mark.xfail(reason="API 7.2 incompatibility, see #4181") class TestStickerSetWithRequest: async def test_create_sticker_set( self, bot, chat_id, sticker_file, animated_sticker_file, video_sticker_file @@ -1120,7 +1115,6 @@ def test_equality(self): assert hash(a) != hash(e) -@pytest.mark.xfail(reason="API 7.2 incompatibility, see #4181") class TestMaskPositionWithRequest(TestMaskPositionBase): async def test_create_new_mask_sticker_set(self, bot, chat_id, sticker_file, mask_position): name = f"masks_by_{bot.username}" From c67f751d07f693e6466989dee0ff487357b3f3a9 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 2 Apr 2024 19:05:34 -0400 Subject: [PATCH 14/30] Make test_drop_callback_data use NonchalantHttpxRequest to avoid flood errors --- tests/ext/test_callbackcontext.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/ext/test_callbackcontext.py b/tests/ext/test_callbackcontext.py index ed4f014b2b2..0a099f64f15 100644 --- a/tests/ext/test_callbackcontext.py +++ b/tests/ext/test_callbackcontext.py @@ -32,6 +32,7 @@ from telegram.error import TelegramError from telegram.ext import ApplicationBuilder, CallbackContext, Job from telegram.warnings import PTBUserWarning +from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots """ @@ -211,8 +212,9 @@ def test_drop_callback_data_exception(self, bot, app): finally: app.bot = bot - async def test_drop_callback_data(self, bot, monkeypatch, chat_id): - app = ApplicationBuilder().token(bot.token).arbitrary_callback_data(True).build() + async def test_drop_callback_data(self, bot, chat_id): + new_bot = make_bot(token=bot.token, arbitrary_callback_data=True) + app = ApplicationBuilder().bot(new_bot).build() update = Update( 0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False)) From 92500176617598f89106a6c79f6e104a1ab8f70b Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 2 Apr 2024 19:27:39 -0400 Subject: [PATCH 15/30] Add Birthdate class and field to Chat --- docs/source/telegram.at-tree.rst | 1 + docs/source/telegram.birthdate.rst | 7 +++ telegram/__init__.py | 2 + telegram/_birthdate.py | 70 ++++++++++++++++++++++++++++++ telegram/_chat.py | 13 ++++++ tests/test_birthdate.py | 62 ++++++++++++++++++++++++++ tests/test_chat.py | 6 +++ 7 files changed, 161 insertions(+) create mode 100644 docs/source/telegram.birthdate.rst create mode 100644 telegram/_birthdate.py create mode 100644 tests/test_birthdate.py diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 19b9831cc76..cf2c7d1cccd 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -6,6 +6,7 @@ Available Types telegram.animation telegram.audio + telegram.birthdate telegram.botcommand telegram.botcommandscope telegram.botcommandscopeallchatadministrators diff --git a/docs/source/telegram.birthdate.rst b/docs/source/telegram.birthdate.rst new file mode 100644 index 00000000000..083de5ebf4a --- /dev/null +++ b/docs/source/telegram.birthdate.rst @@ -0,0 +1,7 @@ +Birthdate +========= + +.. autoclass:: telegram.Birthdate + :members: + :show-inheritance: + diff --git a/telegram/__init__.py b/telegram/__init__.py index 6b9d513b361..f12e5b8da61 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -22,6 +22,7 @@ __all__ = ( "Animation", "Audio", + "Birthdate", "Bot", "BotCommand", "BotCommandScope", @@ -226,6 +227,7 @@ from . import _version, constants, error, helpers, request, warnings +from ._birthdate import Birthdate from ._bot import Bot from ._botcommand import BotCommand from ._botcommandscope import ( diff --git a/telegram/_birthdate.py b/telegram/_birthdate.py new file mode 100644 index 00000000000..6a58b3072b0 --- /dev/null +++ b/telegram/_birthdate.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Birthday.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class Birthdate(TelegramObject): + """ + This object represents a user's birthday. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`day`, and :attr:`month` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + day (:obj:`int`): Day of the user's birth; 1-31. + month (:obj:`int`): Month of the user's birth; 1-12. + year (:obj:`int`, optional): Year of the user's birth. + + Attributes: + day (:obj:`int`): Day of the user's birth; 1-31. + month (:obj:`int`): Month of the user's birth; 1-12. + year (:obj:`int`): Optional. Year of the user's birth. + + """ + + __slots__ = ("day", "month", "year") + + def __init__( + self, + day: int, + month: int, + year: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + # Required + self.day: int = day + self.month: int = month + # Optional + self.year: Optional[int] = year + + self._id_attrs = ( + self.day, + self.month, + ) + + self._freeze() diff --git a/telegram/_chat.py b/telegram/_chat.py index 741b0650d0c..afa8f82cac5 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple, Union from telegram import constants +from telegram._birthdate import Birthdate from telegram._chatlocation import ChatLocation from telegram._chatpermissions import ChatPermissions from telegram._files.chatphoto import ChatPhoto @@ -229,6 +230,10 @@ class Chat(TelegramObject): and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.0 + birthdate (:obj:`telegram.Birthdate`, optional): For private chats, + the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits @@ -372,6 +377,10 @@ class Chat(TelegramObject): and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.0 + birthdate (:obj:`telegram.Birthdate`): Optional. For private chats, + the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups .. _accent colors: https://core.telegram.org/bots/api#accent-colors @@ -383,6 +392,7 @@ class Chat(TelegramObject): "available_reactions", "background_custom_emoji_id", "bio", + "birthdate", "can_set_sticker_set", "custom_emoji_sticker_set_name", "description", @@ -470,6 +480,7 @@ def __init__( has_visible_history: Optional[bool] = None, unrestrict_boost_count: Optional[int] = None, custom_emoji_sticker_set_name: Optional[str] = None, + birthdate: Optional[Birthdate] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -519,6 +530,7 @@ def __init__( self.profile_background_custom_emoji_id: Optional[str] = profile_background_custom_emoji_id self.unrestrict_boost_count: Optional[int] = unrestrict_boost_count self.custom_emoji_sticker_set_name: Optional[str] = custom_emoji_sticker_set_name + self.birthdate: Optional[Birthdate] = birthdate self._id_attrs = (self.id,) @@ -587,6 +599,7 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Chat"]: data["permissions"] = ChatPermissions.de_json(data.get("permissions"), bot) data["location"] = ChatLocation.de_json(data.get("location"), bot) data["available_reactions"] = ReactionType.de_list(data.get("available_reactions"), bot) + data["birthdate"] = Birthdate.de_json(data.get("birthdate"), bot) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility diff --git a/tests/test_birthdate.py b/tests/test_birthdate.py new file mode 100644 index 00000000000..d4ea28b9f7f --- /dev/null +++ b/tests/test_birthdate.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import pytest + +from telegram import Birthdate +from tests.auxil.slots import mro_slots + + +class TestBirthdateBase: + day = 1 + month = 1 + year = 2022 + + +@pytest.fixture(scope="module") +def birthdate(): + return Birthdate(TestBirthdateBase.day, TestBirthdateBase.month, TestBirthdateBase.year) + + +class TestBirthdateWithoutRequest(TestBirthdateBase): + def test_slot_behaviour(self, birthdate): + for attr in birthdate.__slots__: + assert getattr(birthdate, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(birthdate)) == len(set(mro_slots(birthdate))), "duplicate slot" + + def test_to_dict(self, birthdate): + bd_dict = birthdate.to_dict() + assert isinstance(bd_dict, dict) + assert bd_dict["day"] == self.day + assert bd_dict["month"] == self.month + assert bd_dict["year"] == self.year + + def test_equality(self): + bd1 = Birthdate(1, 1, 2022) + bd2 = Birthdate(1, 1, 2022) + bd3 = Birthdate(1, 1, 2023) + bd4 = Birthdate(1, 2, 2022) + + assert bd1 == bd2 + assert hash(bd1) == hash(bd2) + + assert bd1 == bd3 + assert hash(bd1) == hash(bd3) + + assert bd1 != bd4 + assert hash(bd1) != hash(bd4) diff --git a/tests/test_chat.py b/tests/test_chat.py index 7db747146a8..dfa7ca8fc76 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -21,6 +21,7 @@ import pytest from telegram import ( + Birthdate, Bot, Chat, ChatLocation, @@ -74,6 +75,7 @@ def chat(bot): profile_background_custom_emoji_id=TestChatBase.profile_background_custom_emoji_id, unrestrict_boost_count=TestChatBase.unrestrict_boost_count, custom_emoji_sticker_set_name=TestChatBase.custom_emoji_sticker_set_name, + birthdate=Birthdate(1, 1), ) chat.set_bot(bot) chat._unfreeze() @@ -119,6 +121,7 @@ class TestChatBase: profile_background_custom_emoji_id = "profile_background_custom_emoji_id" unrestrict_boost_count = 100 custom_emoji_sticker_set_name = "custom_emoji_sticker_set_name" + birthdate = Birthdate(1, 1) class TestChatWithoutRequest(TestChatBase): @@ -162,6 +165,7 @@ def test_de_json(self, bot): "profile_background_custom_emoji_id": self.profile_background_custom_emoji_id, "unrestrict_boost_count": self.unrestrict_boost_count, "custom_emoji_sticker_set_name": self.custom_emoji_sticker_set_name, + "birthdate": self.birthdate.to_dict(), } chat = Chat.de_json(json_dict, bot) @@ -202,6 +206,7 @@ def test_de_json(self, bot): assert chat.profile_background_custom_emoji_id == self.profile_background_custom_emoji_id assert chat.unrestrict_boost_count == self.unrestrict_boost_count assert chat.custom_emoji_sticker_set_name == self.custom_emoji_sticker_set_name + assert chat.birthdate == self.birthdate def test_de_json_localization(self, bot, raw_bot, tz_bot): json_dict = { @@ -267,6 +272,7 @@ def test_to_dict(self, chat): ) assert chat_dict["custom_emoji_sticker_set_name"] == chat.custom_emoji_sticker_set_name assert chat_dict["unrestrict_boost_count"] == chat.unrestrict_boost_count + assert chat_dict["birthdate"] == chat.birthdate.to_dict() def test_always_tuples_attributes(self): chat = Chat( From a1f86c63f4e0acae58bbb2efadaf0738d180126d Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:00:31 -0400 Subject: [PATCH 16/30] Add replace_sticker_in_set method --- docs/source/inclusions/bot_methods.rst | 2 + telegram/_bot.py | 52 ++++++++++++++++++++++++++ telegram/ext/_extbot.py | 27 +++++++++++++ tests/_files/test_sticker.py | 12 ++++++ tests/test_bot.py | 4 +- 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 11b390bab32..9dcfa1982e2 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -239,6 +239,8 @@ - Used for setting a sticker set of a chat * - :meth:`~telegram.Bot.delete_chat_sticker_set` - Used for deleting the set sticker set of a chat + * - :meth:`~telegram.Bot.replace_sticker_in_set` + - Used for replacing a sticker in a set * - :meth:`~telegram.Bot.set_sticker_position_in_set` - Used for moving a sticker's position in the set * - :meth:`~telegram.Bot.set_sticker_set_title` diff --git a/telegram/_bot.py b/telegram/_bot.py index 9b0e55ae466..d4099992fac 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -8815,6 +8815,56 @@ async def get_business_connection( bot=self, ) + async def replace_sticker_in_set( + self, + user_id: int, + name: str, + old_sticker: str, + sticker: "InputSticker", + *, + 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: Optional[JSONDict] = None, + ) -> bool: + """Use this method to replace an existing sticker in a sticker set with a new one. + The method is equivalent to calling :meth:`delete_sticker_from_set`, + then :meth:`add_sticker_to_set`, then :meth:`set_sticker_position_in_set`. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): User identifier of the sticker set owner. + name (:obj:`str`): Sticker set name. + old_sticker (:obj:`str`): File identifier of the replaced sticker. + sticker (:obj:`telegram.InputSticker`): An object with information about the added + sticker. If exactly the same sticker had already been added to the set, then the + set remains unchanged. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + "name": name, + "old_sticker": old_sticker, + "sticker": sticker, + } + + return await self._post( + "replaceStickerInSet", + 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: """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -9063,3 +9113,5 @@ def to_dict(self, recursive: bool = True) -> JSONDict: """Alias for :meth:`set_message_reaction`""" getBusinessConnection = get_business_connection """Alias for :meth:`get_business_connection`""" + replaceStickerInSet = replace_sticker_in_set + """Alias for :meth:`replace_sticker_in_set`""" diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 7c36b895129..7b5649ebea3 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -4061,6 +4061,32 @@ async def get_business_connection( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def replace_sticker_in_set( + self, + user_id: int, + name: str, + old_sticker: str, + sticker: "InputSticker", + *, + 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: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> bool: + return await super().replace_sticker_in_set( + user_id=user_id, + name=name, + old_sticker=old_sticker, + sticker=sticker, + 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 @@ -4181,3 +4207,4 @@ async def get_business_connection( getUserChatBoosts = get_user_chat_boosts setMessageReaction = set_message_reaction getBusinessConnection = get_business_connection + replaceStickerInSet = replace_sticker_in_set diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index 72be6bafba7..32a1ed9f77e 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -1045,6 +1045,18 @@ async def test_bot_methods_7_webm(self, bot, video_sticker_set): file_id = video_sticker_set.stickers[-1].file_id assert await bot.set_sticker_keywords(file_id, ["test", "test2"]) + async def test_bot_methods_8_png(self, bot, sticker_set, sticker_file): + file_id = sticker_set.stickers[-1].file_id + assert await bot.replace_sticker_in_set( + bot.id, + f"test_by_{bot.username}", + file_id, + sticker=InputSticker( + sticker=sticker_file, + emoji_list=["😄"], + ), + ) + @pytest.fixture(scope="module") def mask_position(): diff --git a/tests/test_bot.py b/tests/test_bot.py index fda1ae559bb..4cee55e044f 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -3378,8 +3378,8 @@ async def test_pin_and_unpin_message(self, bot, super_group_id): assert await bot.unpin_all_chat_messages(super_group_id, read_timeout=10) # get_sticker_set, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, - # set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers - # are tested in the test_sticker module. + # set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers, + # replace_sticker_in_set are tested in the test_sticker module. # get_forum_topic_icon_stickers, edit_forum_topic, general_forum etc... # are tested in the test_forum module. From a9e0eda9adeb633ea1866c0064a3816ed7e49734 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:06:11 -0400 Subject: [PATCH 17/30] Add can_connect_to_business field --- telegram/_user.py | 20 +++++++++++++++++--- tests/test_user.py | 5 +++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/telegram/_user.py b/telegram/_user.py index 19835da245f..8ceb4d0d416 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -78,11 +78,11 @@ class User(TelegramObject): username (:obj:`str`, optional): User's or bot's username. language_code (:obj:`str`, optional): IETF language tag of the user's language. can_join_groups (:obj:`str`, optional): :obj:`True`, if the bot can be invited to groups. - Returned only in :attr:`telegram.Bot.get_me` requests. + Returned only in :meth:`telegram.Bot.get_me`. can_read_all_group_messages (:obj:`str`, optional): :obj:`True`, if privacy mode is - disabled for the bot. Returned only in :attr:`telegram.Bot.get_me` requests. + disabled for the bot. Returned only in :meth:`telegram.Bot.get_me`. supports_inline_queries (:obj:`str`, optional): :obj:`True`, if the bot supports inline - queries. Returned only in :attr:`telegram.Bot.get_me` requests. + queries. Returned only in :meth:`telegram.Bot.get_me`. is_premium (:obj:`bool`, optional): :obj:`True`, if this user is a Telegram Premium user. @@ -91,6 +91,12 @@ class User(TelegramObject): the bot to the attachment menu. .. versionadded:: 20.0 + can_connect_to_business (:obj:`bool`, optional): :obj:`True`, if the bot can be connected + to a Telegram Business account to receive its messages. Returned only in + :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION + Attributes: id (:obj:`int`): Unique identifier for this user or bot. is_bot (:obj:`bool`): :obj:`True`, if this user is a bot. @@ -112,6 +118,11 @@ class User(TelegramObject): the bot to the attachment menu. .. versionadded:: 20.0 + can_connect_to_business (:obj:`bool`): Optional. :obj:`True`, if the bot can be connected + to a Telegram Business account to receive its messages. Returned only in + :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION .. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id` coincides with the :attr:`Chat.id` of the private chat with the user. This has been the case so far, but Telegram does not guarantee that this stays this way. @@ -119,6 +130,7 @@ class User(TelegramObject): __slots__ = ( "added_to_attachment_menu", + "can_connect_to_business", "can_join_groups", "can_read_all_group_messages", "first_name", @@ -144,6 +156,7 @@ def __init__( supports_inline_queries: Optional[bool] = None, is_premium: Optional[bool] = None, added_to_attachment_menu: Optional[bool] = None, + can_connect_to_business: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -161,6 +174,7 @@ def __init__( self.supports_inline_queries: Optional[bool] = supports_inline_queries self.is_premium: Optional[bool] = is_premium self.added_to_attachment_menu: Optional[bool] = added_to_attachment_menu + self.can_connect_to_business: Optional[bool] = can_connect_to_business self._id_attrs = (self.id,) diff --git a/tests/test_user.py b/tests/test_user.py index 9edc0db6b53..86faa73cd77 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -42,6 +42,7 @@ def json_dict(): "supports_inline_queries": TestUserBase.supports_inline_queries, "is_premium": TestUserBase.is_premium, "added_to_attachment_menu": TestUserBase.added_to_attachment_menu, + "can_connect_to_business": TestUserBase.can_connect_to_business, } @@ -59,6 +60,7 @@ def user(bot): supports_inline_queries=TestUserBase.supports_inline_queries, is_premium=TestUserBase.is_premium, added_to_attachment_menu=TestUserBase.added_to_attachment_menu, + can_connect_to_business=TestUserBase.can_connect_to_business, ) user.set_bot(bot) user._unfreeze() @@ -77,6 +79,7 @@ class TestUserBase: supports_inline_queries = False is_premium = True added_to_attachment_menu = False + can_connect_to_business = True class TestUserWithoutRequest(TestUserBase): @@ -100,6 +103,7 @@ def test_de_json(self, json_dict, bot): assert user.supports_inline_queries == self.supports_inline_queries assert user.is_premium == self.is_premium assert user.added_to_attachment_menu == self.added_to_attachment_menu + assert user.can_connect_to_business == self.can_connect_to_business def test_to_dict(self, user): user_dict = user.to_dict() @@ -116,6 +120,7 @@ def test_to_dict(self, user): assert user_dict["supports_inline_queries"] == user.supports_inline_queries assert user_dict["is_premium"] == user.is_premium assert user_dict["added_to_attachment_menu"] == user.added_to_attachment_menu + assert user_dict["can_connect_to_business"] == user.can_connect_to_business def test_equality(self): a = User(self.id_, self.first_name, self.is_bot, self.last_name) From 3d513ecca549869d87fcf46de202140112102e41 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:12:37 -0400 Subject: [PATCH 18/30] Add personal_chat to Chat --- telegram/_chat.py | 12 ++++++++++++ tests/test_birthdate.py | 8 ++++++++ tests/test_chat.py | 5 +++++ 3 files changed, 25 insertions(+) diff --git a/telegram/_chat.py b/telegram/_chat.py index afa8f82cac5..ce78b9e1982 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -233,6 +233,10 @@ class Chat(TelegramObject): birthdate (:obj:`telegram.Birthdate`, optional): For private chats, the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: NEXT.VERSION + personal_chat (:obj:`telegram.Chat`, optional): For private chats, the personal channel of + the user. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: NEXT.VERSION Attributes: @@ -380,6 +384,10 @@ class Chat(TelegramObject): birthdate (:obj:`telegram.Birthdate`): Optional. For private chats, the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: NEXT.VERSION + personal_chat (:obj:`telegram.Chat`): Optional. For private chats, the personal channel of + the user. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: NEXT.VERSION .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups @@ -415,6 +423,7 @@ class Chat(TelegramObject): "location", "message_auto_delete_time", "permissions", + "personal_chat", "photo", "pinned_message", "profile_accent_color_id", @@ -481,6 +490,7 @@ def __init__( unrestrict_boost_count: Optional[int] = None, custom_emoji_sticker_set_name: Optional[str] = None, birthdate: Optional[Birthdate] = None, + personal_chat: Optional["Chat"] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -531,6 +541,7 @@ def __init__( self.unrestrict_boost_count: Optional[int] = unrestrict_boost_count self.custom_emoji_sticker_set_name: Optional[str] = custom_emoji_sticker_set_name self.birthdate: Optional[Birthdate] = birthdate + self.personal_chat: Optional["Chat"] = personal_chat self._id_attrs = (self.id,) @@ -600,6 +611,7 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Chat"]: data["location"] = ChatLocation.de_json(data.get("location"), bot) data["available_reactions"] = ReactionType.de_list(data.get("available_reactions"), bot) data["birthdate"] = Birthdate.de_json(data.get("birthdate"), bot) + data["personal_chat"] = cls.de_json(data.get("personal_chat"), bot) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility diff --git a/tests/test_birthdate.py b/tests/test_birthdate.py index d4ea28b9f7f..096c0ba61a6 100644 --- a/tests/test_birthdate.py +++ b/tests/test_birthdate.py @@ -46,6 +46,14 @@ def test_to_dict(self, birthdate): assert bd_dict["month"] == self.month assert bd_dict["year"] == self.year + def test_de_json(self, bot): + json_dict = {"day": self.day, "month": self.month, "year": self.year} + bd = Birthdate.de_json(json_dict, bot) + assert isinstance(bd, Birthdate) + assert bd.day == self.day + assert bd.month == self.month + assert bd.year == self.year + def test_equality(self): bd1 = Birthdate(1, 1, 2022) bd2 = Birthdate(1, 1, 2022) diff --git a/tests/test_chat.py b/tests/test_chat.py index dfa7ca8fc76..85a78aa1435 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -76,6 +76,7 @@ def chat(bot): unrestrict_boost_count=TestChatBase.unrestrict_boost_count, custom_emoji_sticker_set_name=TestChatBase.custom_emoji_sticker_set_name, birthdate=Birthdate(1, 1), + personal_chat=TestChatBase.personal_chat, ) chat.set_bot(bot) chat._unfreeze() @@ -122,6 +123,7 @@ class TestChatBase: unrestrict_boost_count = 100 custom_emoji_sticker_set_name = "custom_emoji_sticker_set_name" birthdate = Birthdate(1, 1) + personal_chat = Chat(3, "private", "private") class TestChatWithoutRequest(TestChatBase): @@ -166,6 +168,7 @@ def test_de_json(self, bot): "unrestrict_boost_count": self.unrestrict_boost_count, "custom_emoji_sticker_set_name": self.custom_emoji_sticker_set_name, "birthdate": self.birthdate.to_dict(), + "personal_chat": self.personal_chat.to_dict(), } chat = Chat.de_json(json_dict, bot) @@ -207,6 +210,7 @@ def test_de_json(self, bot): assert chat.unrestrict_boost_count == self.unrestrict_boost_count assert chat.custom_emoji_sticker_set_name == self.custom_emoji_sticker_set_name assert chat.birthdate == self.birthdate + assert chat.personal_chat == self.personal_chat def test_de_json_localization(self, bot, raw_bot, tz_bot): json_dict = { @@ -273,6 +277,7 @@ def test_to_dict(self, chat): assert chat_dict["custom_emoji_sticker_set_name"] == chat.custom_emoji_sticker_set_name assert chat_dict["unrestrict_boost_count"] == chat.unrestrict_boost_count assert chat_dict["birthdate"] == chat.birthdate.to_dict() + assert chat_dict["personal_chat"] == chat.personal_chat.to_dict() def test_always_tuples_attributes(self): chat = Chat( From c350584339635f4a785e4ee60a089c7d767e6489 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:17:43 -0400 Subject: [PATCH 19/30] Add filters.IS_FROM_OFFLINE --- telegram/ext/filters.py | 15 +++++++++++++++ tests/ext/test_filters.py | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 5eb700aa89c..db9e9df707f 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -54,6 +54,7 @@ "HAS_PROTECTED_CONTENT", "INVOICE", "IS_AUTOMATIC_FORWARD", + "IS_FROM_OFFLINE", "IS_TOPIC_MESSAGE", "LOCATION", "PASSPORT_DATA", @@ -1559,6 +1560,20 @@ def filter(self, message: Message) -> bool: """ +class _IsFromOffline(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.is_from_offline) + + +IS_FROM_OFFLINE = _IsFromOffline(name="filters.IS_FROM_OFFLINE") +"""Messages that contain :attr:`telegram.Message.is_from_offline`. + + .. versionadded:: NEXT.VERSION +""" + + class Language(MessageFilter): """Filters messages to only allow those which are from users with a certain language code. diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index de9b30cb755..694ea009a6f 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -2035,6 +2035,11 @@ def test_filters_is_automatic_forward(self, update): update.message.is_automatic_forward = True assert filters.IS_AUTOMATIC_FORWARD.check_update(update) + def test_filters_is_from_offline(self, update): + assert not filters.IS_FROM_OFFLINE.check_update(update) + update.message.is_from_offline = True + assert filters.IS_FROM_OFFLINE.check_update(update) + def test_filters_is_topic_message(self, update): assert not filters.IS_TOPIC_MESSAGE.check_update(update) update.message.is_topic_message = True From f927ad0d90b84259e33020ef579ab16f73c26546 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:38:05 -0400 Subject: [PATCH 20/30] Update test_official --- telegram/_files/inputsticker.py | 16 +++++-- tests/_files/test_sticker.py | 58 ++++++++++++++++++-------- tests/request/test_requestparameter.py | 2 +- tests/test_official/exceptions.py | 1 + 4 files changed, 54 insertions(+), 23 deletions(-) diff --git a/telegram/_files/inputsticker.py b/telegram/_files/inputsticker.py index b5189fd4c32..192bef21451 100644 --- a/telegram/_files/inputsticker.py +++ b/telegram/_files/inputsticker.py @@ -36,6 +36,10 @@ class InputSticker(TelegramObject): .. versionadded:: 20.2 + .. versionchanged:: NEXT.VERSION + As of Bot API 7.2, the new argument :paramref:`format` is a required argument, and thus the + order of the arguments has changed. + Args: sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): The added sticker. |uploadinputnopath| Animated and video stickers can't be uploaded via @@ -52,12 +56,14 @@ class InputSticker(TelegramObject): :tg-const:`telegram.constants.StickerLimit.MAX_KEYWORD_LENGTH` characters. For ":tg-const:`telegram.constants.StickerType.REGULAR`" and ":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only. - format (:obj:`str`, optional): Format of the added sticker, must be one of + format (:obj:`str`): Format of the added sticker, must be one of :tg-const:`telegram.constants.StickerFormat.STATIC` for a ``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED` for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM video. + .. versionadded:: NEXT.VERSION + Attributes: sticker (:obj:`str` | :class:`telegram.InputFile`): The added sticker. emoji_list (Tuple[:obj:`str`]): Tuple of @@ -73,11 +79,13 @@ class InputSticker(TelegramObject): ":tg-const:`telegram.constants.StickerType.REGULAR`" and ":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only. ":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only. - format (:obj:`str`): Optional. Format of the added sticker, must be one of + format (:obj:`str`): Format of the added sticker, must be one of :tg-const:`telegram.constants.StickerFormat.STATIC` for a ``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED` for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM video. + + .. versionadded:: NEXT.VERSION """ __slots__ = ("emoji_list", "format", "keywords", "mask_position", "sticker") @@ -86,9 +94,9 @@ def __init__( self, sticker: FileInput, emoji_list: Sequence[str], + format: str, # pylint: disable=redefined-builtin mask_position: Optional[MaskPosition] = None, keywords: Optional[Sequence[str]] = None, - format: Optional[str] = None, # pylint: disable=redefined-builtin *, api_kwargs: Optional[JSONDict] = None, ): @@ -102,8 +110,8 @@ def __init__( attach=True, ) self.emoji_list: Tuple[str, ...] = parse_sequence_arg(emoji_list) + self.format: str = format self.mask_position: Optional[MaskPosition] = mask_position self.keywords: Tuple[str, ...] = parse_sequence_arg(keywords) - self.format: Optional[str] = format self._freeze() diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index 32a1ed9f77e..6013533424c 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -672,7 +672,9 @@ async def make_assertion(_, data, *args, **kwargs): ) monkeypatch.setattr(bot, "_post", make_assertion) - await bot.upload_sticker_file(chat_id, sticker=file, sticker_format="static") + await bot.upload_sticker_file( + chat_id, sticker=file, sticker_format=StickerFormat.STATIC + ) assert test_flag finally: bot._local_mode = False @@ -702,8 +704,7 @@ async def make_assertion(_, data, *args, **kwargs): chat_id, "name", "title", - stickers=[InputSticker(file, emoji_list=["emoji"])], - sticker_format=StickerFormat.STATIC, + stickers=[InputSticker(file, emoji_list=["emoji"], format=StickerFormat.STATIC)], ) assert test_flag @@ -742,7 +743,9 @@ async def make_assertion(_, data, *args, **kwargs): monkeypatch.setattr(bot, "_post", make_assertion) await bot.add_sticker_to_set( - chat_id, "name", sticker=InputSticker(sticker=file, emoji_list=["this"]) + chat_id, + "name", + sticker=InputSticker(sticker=file, emoji_list=["this"], format="static"), ) assert test_flag @@ -820,8 +823,11 @@ async def test_create_sticker_set( chat_id, name=sticker_set, title="Sticker Test", - stickers=[InputSticker(sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.STATIC, + stickers=[ + InputSticker( + sticker_file, emoji_list=["😄"], format=StickerFormat.STATIC + ) + ], ) assert s elif sticker_set.startswith("animated"): @@ -829,8 +835,13 @@ async def test_create_sticker_set( chat_id, name=sticker_set, title="Animated Test", - stickers=[InputSticker(animated_sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.ANIMATED, + stickers=[ + InputSticker( + animated_sticker_file, + emoji_list=["😄"], + format=StickerFormat.ANIMATED, + ) + ], ) assert a elif sticker_set.startswith("video"): @@ -838,8 +849,11 @@ async def test_create_sticker_set( chat_id, name=sticker_set, title="Video Test", - stickers=[InputSticker(video_sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.VIDEO, + stickers=[ + InputSticker( + video_sticker_file, emoji_list=["😄"], format=StickerFormat.VIDEO + ) + ], ) assert v @@ -853,8 +867,7 @@ async def test_delete_sticker_set(self, bot, chat_id, sticker_file): chat_id, name=name, title="Stickerset delete Test", - stickers=[InputSticker(sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.STATIC, + stickers=[InputSticker(sticker_file, emoji_list=["😄"], format="static")], ) # this prevents a second issue when calling delete too soon after creating the set leads # to it failing as well @@ -873,8 +886,11 @@ async def test_set_custom_emoji_sticker_set_thumbnail( chat_id, name=ss_name, title="Custom Emoji Sticker Set", - stickers=[InputSticker(animated_sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.ANIMATED, + stickers=[ + InputSticker( + animated_sticker_file, emoji_list=["😄"], format=StickerFormat.ANIMATED + ) + ], sticker_type=Sticker.CUSTOM_EMOJI, ) assert await bot.set_custom_emoji_sticker_set_thumbnail(ss_name, "") @@ -893,7 +909,9 @@ async def test_bot_methods_1_png(self, bot, chat_id, sticker_file): bot.add_sticker_to_set( chat_id, f"test_by_{bot.username}", - sticker=InputSticker(sticker=file.file_id, emoji_list=["😄"]), + sticker=InputSticker( + sticker=file.file_id, emoji_list=["😄"], format=StickerFormat.STATIC + ), ), bot.add_sticker_to_set( # Also test with file input and mask chat_id, @@ -902,6 +920,7 @@ async def test_bot_methods_1_png(self, bot, chat_id, sticker_file): sticker=sticker_file, emoji_list=["😄"], mask_position=MaskPosition(MaskPosition.EYES, -1, 1, 2), + format=StickerFormat.STATIC, ), ), ) @@ -913,7 +932,9 @@ async def test_bot_methods_1_tgs(self, bot, chat_id): chat_id, f"animated_test_by_{bot.username}", sticker=InputSticker( - sticker=data_file("telegram_animated_sticker.tgs").open("rb"), emoji_list=["😄"] + sticker=data_file("telegram_animated_sticker.tgs").open("rb"), + emoji_list=["😄"], + format=StickerFormat.ANIMATED, ), ) @@ -923,7 +944,7 @@ async def test_bot_methods_1_webm(self, bot, chat_id): assert await bot.add_sticker_to_set( chat_id, f"video_test_by_{bot.username}", - sticker=InputSticker(sticker=f, emoji_list=["🤔"]), + sticker=InputSticker(sticker=f, emoji_list=["🤔"], format=StickerFormat.VIDEO), ) # Test set_sticker_position_in_set @@ -1054,6 +1075,7 @@ async def test_bot_methods_8_png(self, bot, sticker_set, sticker_file): sticker=InputSticker( sticker=sticker_file, emoji_list=["😄"], + format=StickerFormat.STATIC, ), ) @@ -1146,9 +1168,9 @@ async def test_create_new_mask_sticker_set(self, bot, chat_id, sticker_file, mas emoji_list=["😔"], mask_position=mask_position, keywords=["sad"], + format=StickerFormat.STATIC, ) ], - sticker_format=StickerFormat.STATIC, sticker_type=Sticker.MASK, ) assert sticker_set diff --git a/tests/request/test_requestparameter.py b/tests/request/test_requestparameter.py index 59a3e3f9145..d7ad2088a73 100644 --- a/tests/request/test_requestparameter.py +++ b/tests/request/test_requestparameter.py @@ -163,7 +163,7 @@ def test_from_input_inputmedia_without_attach(self): assert request_parameter.input_files == [input_media.media, input_media.thumbnail] def test_from_input_inputsticker(self): - input_sticker = InputSticker(data_file("telegram.png").read_bytes(), ["emoji"]) + input_sticker = InputSticker(data_file("telegram.png").read_bytes(), ["emoji"], "static") expected = input_sticker.to_dict() expected.update({"sticker": input_sticker.sticker.attach_uri}) request_parameter = RequestParameter.from_input("key", input_sticker) diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index edb1ed2ab12..4f69feee467 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -173,6 +173,7 @@ def ignored_param_requirements(object_name: str) -> set[str]: # TODO: Remove this in v21.0 (API 7.1) when this can stop being optional r"ChatAdministratorRights": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, r"ChatMemberAdministrator": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, + "create_new_sticker_set": {"sticker_format"}, # removed by bot api 7.2 } From 275fd3330d941c90b2b58467229700c297fec4e1 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:55:33 -0400 Subject: [PATCH 21/30] Improve test coverage --- tests/_files/test_inputsticker.py | 8 ++++---- tests/test_bot.py | 9 +++++++++ tests/test_update.py | 11 +++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/_files/test_inputsticker.py b/tests/_files/test_inputsticker.py index c627cb56b10..680a0167099 100644 --- a/tests/_files/test_inputsticker.py +++ b/tests/_files/test_inputsticker.py @@ -45,7 +45,7 @@ class TestInputStickerBase: format = "static" -class TestInputStickerNoRequest(TestInputStickerBase): +class TestInputStickerWithoutRequest(TestInputStickerBase): def test_slot_behaviour(self, input_sticker): inst = input_sticker for attr in inst.__slots__: @@ -63,7 +63,7 @@ def test_expected_values(self, input_sticker): def test_attributes_tuple(self, input_sticker): assert isinstance(input_sticker.keywords, tuple) assert isinstance(input_sticker.emoji_list, tuple) - a = InputSticker("sticker", ["emoji"]) + a = InputSticker("sticker", ["emoji"], "static") assert isinstance(a.emoji_list, tuple) assert a.keywords == () @@ -78,7 +78,7 @@ def test_to_dict(self, input_sticker): assert input_sticker_dict["format"] == input_sticker.format def test_with_sticker_input_types(self, video_sticker_file): # noqa: F811 - sticker = InputSticker(sticker=video_sticker_file, emoji_list=["👍"]) + sticker = InputSticker(sticker=video_sticker_file, emoji_list=["👍"], format="video") assert isinstance(sticker.sticker, InputFile) - sticker = InputSticker(data_file("telegram_video_sticker.webm"), ["👍"]) + sticker = InputSticker(data_file("telegram_video_sticker.webm"), ["👍"], "video") assert sticker.sticker == data_file("telegram_video_sticker.webm").as_uri() diff --git a/tests/test_bot.py b/tests/test_bot.py index 4cee55e044f..be7e2e156a8 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2087,6 +2087,15 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): api_kwargs={"chat_id": 2, "user_id": 32, "until_date": until_timestamp}, ) + async def test_business_connection_id_argument(self, bot, monkeypatch): + """We can't connect to a business acc, so we just test that the correct data is passed.""" + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters.get("business_connection_id") == 42 + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.send_chat_action(1, "typing", business_connection_id=42) + class TestBotWithRequest: """ diff --git a/tests/test_update.py b/tests/test_update.py index ba6c39ae1bb..b314c98e819 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -136,6 +136,13 @@ (1, 2), ) +business_message = Message( + 1, + datetime.utcnow(), + Chat(1, ""), + User(1, "", False), +) + params = [ {"message": message}, @@ -169,6 +176,8 @@ {"message_reaction_count": message_reaction_count}, {"business_connection": business_connection}, {"deleted_business_messages": deleted_business_messages}, + {"business_message": business_message}, + {"edited_business_message": business_message}, # Must be last to conform with `ids` below! {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, ] @@ -194,6 +203,8 @@ "message_reaction_count", "business_connection", "deleted_business_messages", + "business_message", + "edited_business_message", ) ids = (*all_types, "callback_query_without_message") From b3becd6b0595759a4931b209dfa71e4149c92747 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Wed, 3 Apr 2024 22:37:35 -0400 Subject: [PATCH 22/30] Doc fix for the new handlers --- telegram/ext/_handlers/businessconnectionhandler.py | 4 ++-- telegram/ext/_handlers/businessmessagesdeletedhandler.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/telegram/ext/_handlers/businessconnectionhandler.py b/telegram/ext/_handlers/businessconnectionhandler.py index 0d7833ffae3..a6f2cc01923 100644 --- a/telegram/ext/_handlers/businessconnectionhandler.py +++ b/telegram/ext/_handlers/businessconnectionhandler.py @@ -42,10 +42,10 @@ class BusinessConnectionHandler(BaseHandler[Update, CCT]): async def callback(update: Update, context: CallbackContext) user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only - those which are asking to join the specified user ID(s). + those are from the specified user ID(s). username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only - those which are asking to join the specified username(s). + those which are from the specified username(s). block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in diff --git a/telegram/ext/_handlers/businessmessagesdeletedhandler.py b/telegram/ext/_handlers/businessmessagesdeletedhandler.py index 10785afec37..9dabdf2ac60 100644 --- a/telegram/ext/_handlers/businessmessagesdeletedhandler.py +++ b/telegram/ext/_handlers/businessmessagesdeletedhandler.py @@ -42,10 +42,10 @@ class BusinessMessagesDeletedHandler(BaseHandler[Update, CCT]): async def callback(update: Update, context: CallbackContext) chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only - those which are asking to join the specified chat ID(s). + those which are from the specified chat ID(s). username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only - those which are asking to join the specified username(s). + those which are from the specified username(s). block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in From 9305a4319cade01fd54bc7e72eeb474bc9191f91 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Wed, 3 Apr 2024 22:41:55 -0400 Subject: [PATCH 23/30] change business_connection_id test target --- tests/test_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index be7e2e156a8..32a82bdf428 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2094,7 +2094,7 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.parameters.get("business_connection_id") == 42 monkeypatch.setattr(bot.request, "post", make_assertion) - assert await bot.send_chat_action(1, "typing", business_connection_id=42) + assert await bot.send_message(2, "text", business_connection_id=42) class TestBotWithRequest: From bb9bdb4b8bf3c59fd23ae264fcebdc62cfe991f2 Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Sat, 6 Apr 2024 18:14:06 +0300 Subject: [PATCH 24/30] Business info (#4183) Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> --- docs/source/telegram.at-tree.rst | 4 + docs/source/telegram.businessintro.rst | 6 + docs/source/telegram.businesslocation.rst | 6 + docs/source/telegram.businessopeninghours.rst | 6 + .../telegram.businessopeninghoursinterval.rst | 6 + telegram/__init__.py | 13 +- telegram/_business.py | 262 +++++++++++++++++- telegram/_chat.py | 54 +++- tests/test_business.py | 242 +++++++++++++++- tests/test_chat.py | 22 ++ 10 files changed, 617 insertions(+), 4 deletions(-) create mode 100644 docs/source/telegram.businessintro.rst create mode 100644 docs/source/telegram.businesslocation.rst create mode 100644 docs/source/telegram.businessopeninghours.rst create mode 100644 docs/source/telegram.businessopeninghoursinterval.rst diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index cf2c7d1cccd..fbc5ae7bd9c 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -20,6 +20,10 @@ Available Types telegram.botname telegram.botshortdescription telegram.businessconnection + telegram.businessintro + telegram.businesslocation + telegram.businessopeninghours + telegram.businessopeninghoursinterval telegram.businessmessagesdeleted telegram.callbackquery telegram.chat diff --git a/docs/source/telegram.businessintro.rst b/docs/source/telegram.businessintro.rst new file mode 100644 index 00000000000..4870258e5b4 --- /dev/null +++ b/docs/source/telegram.businessintro.rst @@ -0,0 +1,6 @@ +BusinessIntro +================== + +.. autoclass:: telegram.BusinessIntro + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.businesslocation.rst b/docs/source/telegram.businesslocation.rst new file mode 100644 index 00000000000..1a1b8893b65 --- /dev/null +++ b/docs/source/telegram.businesslocation.rst @@ -0,0 +1,6 @@ +BusinessLocation +================== + +.. autoclass:: telegram.BusinessLocation + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.businessopeninghours.rst b/docs/source/telegram.businessopeninghours.rst new file mode 100644 index 00000000000..cab989c8475 --- /dev/null +++ b/docs/source/telegram.businessopeninghours.rst @@ -0,0 +1,6 @@ +BusinessOpeningHours +==================== + +.. autoclass:: telegram.BusinessOpeningHours + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.businessopeninghoursinterval.rst b/docs/source/telegram.businessopeninghoursinterval.rst new file mode 100644 index 00000000000..241379dbcfb --- /dev/null +++ b/docs/source/telegram.businessopeninghoursinterval.rst @@ -0,0 +1,6 @@ +BusinessOpeningHoursInterval +============================ + +.. autoclass:: telegram.BusinessOpeningHoursInterval + :members: + :show-inheritance: \ No newline at end of file diff --git a/telegram/__init__.py b/telegram/__init__.py index f12e5b8da61..89104373100 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -37,7 +37,11 @@ "BotName", "BotShortDescription", "BusinessConnection", + "BusinessIntro", + "BusinessLocation", "BusinessMessagesDeleted", + "BusinessOpeningHours", + "BusinessOpeningHoursInterval", "CallbackGame", "CallbackQuery", "Chat", @@ -242,7 +246,14 @@ ) from ._botdescription import BotDescription, BotShortDescription from ._botname import BotName -from ._business import BusinessConnection, BusinessMessagesDeleted +from ._business import ( + BusinessConnection, + BusinessIntro, + BusinessLocation, + BusinessMessagesDeleted, + BusinessOpeningHours, + BusinessOpeningHoursInterval, +) from ._callbackquery import CallbackQuery from ._chat import Chat from ._chatadministratorrights import ChatAdministratorRights diff --git a/telegram/_business.py b/telegram/_business.py index 953d5d967a6..d208de31092 100644 --- a/telegram/_business.py +++ b/telegram/_business.py @@ -20,9 +20,11 @@ """This module contains the Telegram Business related classes.""" from datetime import datetime -from typing import TYPE_CHECKING, Optional, Sequence +from typing import TYPE_CHECKING, Optional, Sequence, Tuple from telegram._chat import Chat +from telegram._files.location import Location +from telegram._files.sticker import Sticker from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg @@ -183,3 +185,261 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessMess data["chat"] = Chat.de_json(data.get("chat"), bot) return super().de_json(data=data, bot=bot) + + +class BusinessIntro(TelegramObject): + """ + This object represents the intro of a business account. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`title`, :attr:`message` and :attr:`sticker` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + title (:obj:`str`, optional): Title text of the business intro. + message (:obj:`str`, optional): Message text of the business intro. + sticker (:class:`telegram.Sticker`, optional): Sticker of the business intro. + + Attributes: + title (:obj:`str`): Optional. Title text of the business intro. + message (:obj:`str`): Optional. Message text of the business intro. + sticker (:class:`telegram.Sticker`): Optional. Sticker of the business intro. + """ + + __slots__ = ( + "message", + "sticker", + "title", + ) + + def __init__( + self, + title: Optional[str] = None, + message: Optional[str] = None, + sticker: Optional[Sticker] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.title: Optional[str] = title + self.message: Optional[str] = message + self.sticker: Optional[Sticker] = sticker + + self._id_attrs = (self.title, self.message, self.sticker) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessIntro"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["sticker"] = Sticker.de_json(data.get("sticker"), bot) + + return super().de_json(data=data, bot=bot) + + +class BusinessLocation(TelegramObject): + """ + This object represents the location of a business account. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`address` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + address (:obj:`str`): Address of the business. + location (:class:`telegram.Location`, optional): Location of the business. + + Attributes: + address (:obj:`str`): Address of the business. + location (:class:`telegram.Location`): Optional. Location of the business. + """ + + __slots__ = ( + "address", + "location", + ) + + def __init__( + self, + address: str, + location: Optional[Location] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.address: str = address + self.location: Optional[Location] = location + + self._id_attrs = (self.address,) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessLocation"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["location"] = Location.de_json(data.get("location"), bot) + + return super().de_json(data=data, bot=bot) + + +class BusinessOpeningHoursInterval(TelegramObject): + """ + This object represents the time intervals describing business opening hours. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`opening_minute` and :attr:`closing_minute` are equal. + + .. versionadded:: NEXT.VERSION + + Examples: + A day has (24 * 60 =) 1440 minutes, a week has (7 * 1440 =) 10080 minutes. + Starting the the minute's sequence from Monday, example values of + :attr:`opening_minute`, :attr:`closing_minute` will map to the following day times: + + * Monday - 8am to 8:30pm: + - ``opening_minute = 480`` :guilabel:`8 * 60` + - ``closing_minute = 1230`` :guilabel:`20 * 60 + 30` + * Tuesday - 24 hours: + - ``opening_minute = 1440`` :guilabel:`24 * 60` + - ``closing_minute = 2879`` :guilabel:`2 * 24 * 60 - 1` + * Sunday - 12am - 11:58pm: + - ``opening_minute = 8640`` :guilabel:`6 * 24 * 60` + - ``closing_minute = 10078`` :guilabel:`7 * 24 * 60 - 2` + + Args: + opening_minute (:obj:`int`): The minute's sequence number in a week, starting on Monday, + marking the start of the time interval during which the business is open; + 0 - 7 * 24 * 60. + closing_minute (:obj:`int`): The minute's + sequence number in a week, starting on Monday, marking the end of the time interval + during which the business is open; 0 - 8 * 24 * 60 + + Attributes: + opening_minute (:obj:`int`): The minute's sequence number in a week, starting on Monday, + marking the start of the time interval during which the business is open; + 0 - 7 * 24 * 60. + closing_minute (:obj:`int`): The minute's + sequence number in a week, starting on Monday, marking the end of the time interval + during which the business is open; 0 - 8 * 24 * 60 + """ + + __slots__ = ("_closing_time", "_opening_time", "closing_minute", "opening_minute") + + def __init__( + self, + opening_minute: int, + closing_minute: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.opening_minute: int = opening_minute + self.closing_minute: int = closing_minute + + self._opening_time: Optional[Tuple[int, int, int]] = None + self._closing_time: Optional[Tuple[int, int, int]] = None + + self._id_attrs = (self.opening_minute, self.closing_minute) + + self._freeze() + + def _parse_minute(self, minute: int) -> Tuple[int, int, int]: + return (minute // 1440, minute % 1440 // 60, minute % 1440 % 60) + + @property + def opening_time(self) -> Tuple[int, int, int]: + """Convenience attribute. A :obj:`tuple` parsed from :attr:`opening_minute`. It contains + the `weekday`, `hour` and `minute` in the same ranges as :attr:`datetime.datetime.weekday`, + :attr:`datetime.datetime.hour` and :attr:`datetime.datetime.minute` + + Returns: + Tuple[:obj:`int`, :obj:`int`, :obj:`int`]: + """ + if self._opening_time is None: + self._opening_time = self._parse_minute(self.opening_minute) + return self._opening_time + + @property + def closing_time(self) -> Tuple[int, int, int]: + """Convenience attribute. A :obj:`tuple` parsed from :attr:`closing_minute`. It contains + the `weekday`, `hour` and `minute` in the same ranges as :attr:`datetime.datetime.weekday`, + :attr:`datetime.datetime.hour` and :attr:`datetime.datetime.minute` + + Returns: + Tuple[:obj:`int`, :obj:`int`, :obj:`int`]: + """ + if self._closing_time is None: + self._closing_time = self._parse_minute(self.closing_minute) + return self._closing_time + + +class BusinessOpeningHours(TelegramObject): + """ + This object represents the opening hours of a business account. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`time_zone_name` and :attr:`opening_hours` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + time_zone_name (:obj:`str`): Unique name of the time zone for which the opening + hours are defined. + opening_hours (Sequence[:class:`telegram.BusinessOpeningHoursInterval`]): List of + time intervals describing business opening hours. + + Attributes: + time_zone_name (:obj:`str`): Unique name of the time zone for which the opening + hours are defined. + opening_hours (Sequence[:class:`telegram.BusinessOpeningHoursInterval`]): List of + time intervals describing business opening hours. + """ + + __slots__ = ("opening_hours", "time_zone_name") + + def __init__( + self, + time_zone_name: str, + opening_hours: Sequence[BusinessOpeningHoursInterval], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.time_zone_name: str = time_zone_name + self.opening_hours: Sequence[BusinessOpeningHoursInterval] = parse_sequence_arg( + opening_hours + ) + + self._id_attrs = (self.time_zone_name, self.opening_hours) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessOpeningHours"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["opening_hours"] = BusinessOpeningHoursInterval.de_list( + data.get("opening_hours"), bot + ) + + return super().de_json(data=data, bot=bot) diff --git a/telegram/_chat.py b/telegram/_chat.py index ce78b9e1982..a6b7928e157 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -45,6 +45,9 @@ Animation, Audio, Bot, + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, ChatInviteLink, ChatMember, Contact, @@ -170,6 +173,21 @@ class Chat(TelegramObject): only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 + business_intro (:class:`telegram.BusinessIntro`, optional): For private chats with + business accounts, the intro of the business. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION + business_location (:class:`telegram.BusinessLocation`, optional): For private chats with + business accounts, the location of the business. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION + business_opening_hours (:class:`telegram.BusinessOpeningHours`, optional): For private + chats with business accounts, the opening hours of the business. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION available_reactions (Sequence[:class:`telegram.ReactionType`], optional): List of available reactions allowed in the chat. If omitted, then all of :const:`telegram.constants.ReactionEmoji` are allowed. Returned only in @@ -321,6 +339,21 @@ class Chat(TelegramObject): obtained via :meth:`~telegram.Bot.get_chat`. .. versionadded:: 20.0 + business_intro (:class:`telegram.BusinessIntro`): Optional. For private chats with + business accounts, the intro of the business. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION + business_location (:class:`telegram.BusinessLocation`): Optional. For private chats with + business accounts, the location of the business. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION + business_opening_hours (:class:`telegram.BusinessOpeningHours`): Optional. For private + chats with business accounts, the opening hours of the business. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION available_reactions (Tuple[:class:`telegram.ReactionType`]): Optional. List of available reactions allowed in the chat. If omitted, then all of :const:`telegram.constants.ReactionEmoji` are allowed. Returned only in @@ -401,6 +434,9 @@ class Chat(TelegramObject): "background_custom_emoji_id", "bio", "birthdate", + "business_intro", + "business_location", + "business_opening_hours", "can_set_sticker_set", "custom_emoji_sticker_set_name", "description", @@ -491,6 +527,9 @@ def __init__( custom_emoji_sticker_set_name: Optional[str] = None, birthdate: Optional[Birthdate] = None, personal_chat: Optional["Chat"] = None, + business_intro: Optional["BusinessIntro"] = None, + business_location: Optional["BusinessLocation"] = None, + business_opening_hours: Optional["BusinessOpeningHours"] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -542,6 +581,9 @@ def __init__( self.custom_emoji_sticker_set_name: Optional[str] = custom_emoji_sticker_set_name self.birthdate: Optional[Birthdate] = birthdate self.personal_chat: Optional["Chat"] = personal_chat + self.business_intro: Optional["BusinessIntro"] = business_intro + self.business_location: Optional["BusinessLocation"] = business_location + self.business_opening_hours: Optional["BusinessOpeningHours"] = business_opening_hours self._id_attrs = (self.id,) @@ -604,7 +646,12 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Chat"]: ) data["photo"] = ChatPhoto.de_json(data.get("photo"), bot) - from telegram import Message # pylint: disable=import-outside-toplevel + from telegram import ( # pylint: disable=import-outside-toplevel + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, + Message, + ) data["pinned_message"] = Message.de_json(data.get("pinned_message"), bot) data["permissions"] = ChatPermissions.de_json(data.get("permissions"), bot) @@ -612,6 +659,11 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Chat"]: data["available_reactions"] = ReactionType.de_list(data.get("available_reactions"), bot) data["birthdate"] = Birthdate.de_json(data.get("birthdate"), bot) data["personal_chat"] = cls.de_json(data.get("personal_chat"), bot) + data["business_intro"] = BusinessIntro.de_json(data.get("business_intro"), bot) + data["business_location"] = BusinessLocation.de_json(data.get("business_location"), bot) + data["business_opening_hours"] = BusinessOpeningHours.de_json( + data.get("business_opening_hours"), bot + ) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility diff --git a/tests/test_business.py b/tests/test_business.py index 5514847a753..da6838d6d47 100644 --- a/tests/test_business.py +++ b/tests/test_business.py @@ -20,7 +20,18 @@ import pytest -from telegram import BusinessConnection, BusinessMessagesDeleted, Chat, User +from telegram import ( + BusinessConnection, + BusinessIntro, + BusinessLocation, + BusinessMessagesDeleted, + BusinessOpeningHours, + BusinessOpeningHoursInterval, + Chat, + Location, + Sticker, + User, +) from telegram._utils.datetime import UTC, to_timestamp from tests.auxil.slots import mro_slots @@ -35,6 +46,17 @@ class TestBusinessBase: message_ids = (123, 321) business_connection_id = "123" chat = Chat(123, "test_chat") + title = "Business Title" + message = "Business description" + sticker = Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR) + address = "address" + location = Location(-23.691288, 46.788279) + opening_minute = 0 + closing_minute = 60 + time_zone_name = "Country/City" + opening_hours = [ + BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60) + ] @pytest.fixture(scope="module") @@ -58,6 +80,39 @@ def business_messages_deleted(): ) +@pytest.fixture(scope="module") +def business_intro(): + return BusinessIntro( + TestBusinessBase.title, + TestBusinessBase.message, + TestBusinessBase.sticker, + ) + + +@pytest.fixture(scope="module") +def business_location(): + return BusinessLocation( + TestBusinessBase.address, + TestBusinessBase.location, + ) + + +@pytest.fixture(scope="module") +def business_opening_hours_interval(): + return BusinessOpeningHoursInterval( + TestBusinessBase.opening_minute, + TestBusinessBase.closing_minute, + ) + + +@pytest.fixture(scope="module") +def business_opening_hours(): + return BusinessOpeningHours( + TestBusinessBase.time_zone_name, + TestBusinessBase.opening_hours, + ) + + class TestBusinessConnectionWithoutRequest(TestBusinessBase): def test_slots(self, business_connection): bc = business_connection @@ -170,3 +225,188 @@ def test_equality(self): assert bmd1 != bmd3 assert hash(bmd1) != hash(bmd3) + + +class TestBusinessIntroWithoutRequest(TestBusinessBase): + def test_slot_behaviour(self, business_intro): + intro = business_intro + for attr in intro.__slots__: + assert getattr(intro, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(intro)) == len(set(mro_slots(intro))), "duplicate slot" + + def test_to_dict(self, business_intro): + intro_dict = business_intro.to_dict() + assert isinstance(intro_dict, dict) + assert intro_dict["title"] == self.title + assert intro_dict["message"] == self.message + assert intro_dict["sticker"] == self.sticker.to_dict() + + def test_de_json(self): + json_dict = { + "title": self.title, + "message": self.message, + "sticker": self.sticker.to_dict(), + } + intro = BusinessIntro.de_json(json_dict, None) + assert intro.title == self.title + assert intro.message == self.message + assert intro.sticker == self.sticker + assert intro.api_kwargs == {} + assert isinstance(intro, BusinessIntro) + + def test_equality(self): + intro1 = BusinessIntro(self.title, self.message, self.sticker) + intro2 = BusinessIntro(self.title, self.message, self.sticker) + intro3 = BusinessIntro("Other Business", self.message, self.sticker) + + assert intro1 == intro2 + assert hash(intro1) == hash(intro2) + assert intro1 is not intro2 + + assert intro1 != intro3 + assert hash(intro1) != hash(intro3) + + +class TestBusinessLocationWithoutRequest(TestBusinessBase): + def test_slot_behaviour(self, business_location): + inst = business_location + 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_to_dict(self, business_location): + blc_dict = business_location.to_dict() + assert isinstance(blc_dict, dict) + assert blc_dict["address"] == self.address + assert blc_dict["location"] == self.location.to_dict() + + def test_de_json(self): + json_dict = { + "address": self.address, + "location": self.location.to_dict(), + } + blc = BusinessLocation.de_json(json_dict, None) + assert blc.address == self.address + assert blc.location == self.location + assert blc.api_kwargs == {} + assert isinstance(blc, BusinessLocation) + + def test_equality(self): + blc1 = BusinessLocation(self.address, self.location) + blc2 = BusinessLocation(self.address, self.location) + blc3 = BusinessLocation("Other Address", self.location) + + assert blc1 == blc2 + assert hash(blc1) == hash(blc2) + assert blc1 is not blc2 + + assert blc1 != blc3 + assert hash(blc1) != hash(blc3) + + +class TestBusinessOpeningHoursIntervalWithoutRequest(TestBusinessBase): + def test_slot_behaviour(self, business_opening_hours_interval): + inst = business_opening_hours_interval + 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_to_dict(self, business_opening_hours_interval): + bohi_dict = business_opening_hours_interval.to_dict() + assert isinstance(bohi_dict, dict) + assert bohi_dict["opening_minute"] == self.opening_minute + assert bohi_dict["closing_minute"] == self.closing_minute + + def test_de_json(self): + json_dict = { + "opening_minute": self.opening_minute, + "closing_minute": self.closing_minute, + } + bohi = BusinessOpeningHoursInterval.de_json(json_dict, None) + assert bohi.opening_minute == self.opening_minute + assert bohi.closing_minute == self.closing_minute + assert bohi.api_kwargs == {} + assert isinstance(bohi, BusinessOpeningHoursInterval) + + def test_equality(self): + bohi1 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute) + bohi2 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute) + bohi3 = BusinessOpeningHoursInterval(61, 100) + + assert bohi1 == bohi2 + assert hash(bohi1) == hash(bohi2) + assert bohi1 is not bohi2 + + assert bohi1 != bohi3 + assert hash(bohi1) != hash(bohi3) + + @pytest.mark.parametrize( + ("opening_minute", "expected"), + [ # openings per docstring + (8 * 60, (0, 8, 0)), + (24 * 60, (1, 0, 0)), + (6 * 24 * 60, (6, 0, 0)), + ], + ) + def test_opening_time(self, opening_minute, expected): + bohi = BusinessOpeningHoursInterval(opening_minute, -0) + + opening_time = bohi.opening_time + assert opening_time == expected + + cached = bohi.opening_time + assert cached is opening_time + + @pytest.mark.parametrize( + ("closing_minute", "expected"), + [ # closings per docstring + (20 * 60 + 30, (0, 20, 30)), + (2 * 24 * 60 - 1, (1, 23, 59)), + (7 * 24 * 60 - 2, (6, 23, 58)), + ], + ) + def test_closing_time(self, closing_minute, expected): + bohi = BusinessOpeningHoursInterval(-0, closing_minute) + + closing_time = bohi.closing_time + assert closing_time == expected + + cached = bohi.closing_time + assert cached is closing_time + + +class TestBusinessOpeningHoursWithoutRequest(TestBusinessBase): + def test_slot_behaviour(self, business_opening_hours): + inst = business_opening_hours + 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_to_dict(self, business_opening_hours): + boh_dict = business_opening_hours.to_dict() + assert isinstance(boh_dict, dict) + assert boh_dict["time_zone_name"] == self.time_zone_name + assert boh_dict["opening_hours"] == [opening.to_dict() for opening in self.opening_hours] + + def test_de_json(self): + json_dict = { + "time_zone_name": self.time_zone_name, + "opening_hours": [opening.to_dict() for opening in self.opening_hours], + } + boh = BusinessOpeningHours.de_json(json_dict, None) + assert boh.time_zone_name == self.time_zone_name + assert boh.opening_hours == tuple(self.opening_hours) + assert boh.api_kwargs == {} + assert isinstance(boh, BusinessOpeningHours) + + def test_equality(self): + boh1 = BusinessOpeningHours(self.time_zone_name, self.opening_hours) + boh2 = BusinessOpeningHours(self.time_zone_name, self.opening_hours) + boh3 = BusinessOpeningHours("Other/Timezone", self.opening_hours) + + assert boh1 == boh2 + assert hash(boh1) == hash(boh2) + assert boh1 is not boh2 + + assert boh1 != boh3 + assert hash(boh1) != hash(boh3) diff --git a/tests/test_chat.py b/tests/test_chat.py index 85a78aa1435..11ef38dda15 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -23,6 +23,10 @@ from telegram import ( Birthdate, Bot, + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, + BusinessOpeningHoursInterval, Chat, ChatLocation, ChatPermissions, @@ -75,6 +79,9 @@ def chat(bot): profile_background_custom_emoji_id=TestChatBase.profile_background_custom_emoji_id, unrestrict_boost_count=TestChatBase.unrestrict_boost_count, custom_emoji_sticker_set_name=TestChatBase.custom_emoji_sticker_set_name, + business_intro=TestChatBase.business_intro, + business_location=TestChatBase.business_location, + business_opening_hours=TestChatBase.business_opening_hours, birthdate=Birthdate(1, 1), personal_chat=TestChatBase.personal_chat, ) @@ -116,6 +123,12 @@ class TestChatBase: ReactionTypeEmoji(ReactionEmoji.THUMBS_DOWN), ReactionTypeCustomEmoji("custom_emoji_id"), ] + business_intro = BusinessIntro("Title", "Description", None) + business_location = BusinessLocation("Address", Location(123, 456)) + business_opening_hours = BusinessOpeningHours( + "Country/City", + [BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60)], + ) accent_color_id = 1 background_custom_emoji_id = "background_custom_emoji_id" profile_accent_color_id = 2 @@ -144,6 +157,9 @@ def test_de_json(self, bot): "permissions": self.permissions.to_dict(), "slow_mode_delay": self.slow_mode_delay, "bio": self.bio, + "business_intro": self.business_intro.to_dict(), + "business_location": self.business_location.to_dict(), + "business_opening_hours": self.business_opening_hours.to_dict(), "has_protected_content": self.has_protected_content, "has_visible_history": self.has_visible_history, "has_private_forwards": self.has_private_forwards, @@ -181,6 +197,9 @@ def test_de_json(self, bot): assert chat.permissions == self.permissions assert chat.slow_mode_delay == self.slow_mode_delay assert chat.bio == self.bio + assert chat.business_intro == self.business_intro + assert chat.business_location == self.business_location + assert chat.business_opening_hours == self.business_opening_hours assert chat.has_protected_content == self.has_protected_content assert chat.has_visible_history == self.has_visible_history assert chat.has_private_forwards == self.has_private_forwards @@ -243,6 +262,9 @@ def test_to_dict(self, chat): assert chat_dict["permissions"] == chat.permissions.to_dict() assert chat_dict["slow_mode_delay"] == chat.slow_mode_delay assert chat_dict["bio"] == chat.bio + assert chat_dict["business_intro"] == chat.business_intro.to_dict() + assert chat_dict["business_location"] == chat.business_location.to_dict() + assert chat_dict["business_opening_hours"] == chat.business_opening_hours.to_dict() assert chat_dict["has_private_forwards"] == chat.has_private_forwards assert chat_dict["has_protected_content"] == chat.has_protected_content assert chat_dict["has_visible_history"] == chat.has_visible_history From 0f99ace0145cb5478a9386db5dc2e9ebd84f3691 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sun, 7 Apr 2024 07:14:30 +0530 Subject: [PATCH 25/30] Request Chat Improvements (tests remaining) --- docs/source/telegram.at-tree.rst | 1 + docs/source/telegram.shareduser.rst | 7 + telegram/__init__.py | 3 +- telegram/_keyboardbuttonrequest.py | 57 +++++++- telegram/_shared.py | 194 ++++++++++++++++++++++++++-- 5 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 docs/source/telegram.shareduser.rst diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index fbc5ae7bd9c..3d78292588a 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -114,6 +114,7 @@ Available Types telegram.replykeyboardremove telegram.replyparameters telegram.sentwebappmessage + telegram.shareduser telegram.story telegram.switchinlinequerychosenchat telegram.telegramobject diff --git a/docs/source/telegram.shareduser.rst b/docs/source/telegram.shareduser.rst new file mode 100644 index 00000000000..52dd3885bc0 --- /dev/null +++ b/docs/source/telegram.shareduser.rst @@ -0,0 +1,7 @@ +SharedUser +========== + +.. autoclass:: telegram.SharedUser + :members: + :show-inheritance: + diff --git a/telegram/__init__.py b/telegram/__init__.py index 89104373100..304425c4d61 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -191,6 +191,7 @@ "SecureData", "SecureValue", "SentWebAppMessage", + "SharedUser", "ShippingAddress", "ShippingOption", "ShippingQuery", @@ -409,7 +410,7 @@ from ._replykeyboardmarkup import ReplyKeyboardMarkup from ._replykeyboardremove import ReplyKeyboardRemove from ._sentwebappmessage import SentWebAppMessage -from ._shared import ChatShared, UsersShared +from ._shared import ChatShared, SharedUser, UsersShared from ._story import Story from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject diff --git a/telegram/_keyboardbuttonrequest.py b/telegram/_keyboardbuttonrequest.py index 78bb2e50545..10f80cd65da 100644 --- a/telegram/_keyboardbuttonrequest.py +++ b/telegram/_keyboardbuttonrequest.py @@ -56,6 +56,15 @@ class KeyboardButtonRequestUsers(TelegramObject): . .. versionadded:: 20.8 + request_name (:obj:`bool`, optional): Pass :obj:`True` to request the users' first and last name. + + .. versionadded:: NEXT.VERSION + request_username (:obj:`bool`, optional): Pass :obj:`True` to request the users' username. + + .. versionadded:: NEXT.VERSION + request_photo (:obj:`bool`, optional): Pass :obj:`True` to request the users' photo. + + .. versionadded:: NEXT.VERSION Attributes: request_id (:obj:`int`): Identifier of the request. @@ -71,6 +80,16 @@ class KeyboardButtonRequestUsers(TelegramObject): . .. versionadded:: 20.8 + request_name (:obj:`bool`): Optional. Pass :obj:`True` to request the users' first and last name. + + .. versionadded:: NEXT.VERSION + request_username (:obj:`bool`): Optional. Pass :obj:`True` to request the users' username. + + .. versionadded:: NEXT.VERSION + request_photo (:obj:`bool`): Optional. Pass :obj:`True` to request the users' photo. + + .. versionadded:: NEXT.VERSION + """ __slots__ = ( @@ -78,6 +97,9 @@ class KeyboardButtonRequestUsers(TelegramObject): "request_id", "user_is_bot", "user_is_premium", + "request_name", + "request_username", + "request_photo", ) def __init__( @@ -86,6 +108,9 @@ def __init__( user_is_bot: Optional[bool] = None, user_is_premium: Optional[bool] = None, max_quantity: Optional[int] = None, + request_name: Optional[bool]= None, + request_username: Optional[bool]= None, + request_photo: Optional[bool]= None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -97,6 +122,9 @@ def __init__( self.user_is_bot: Optional[bool] = user_is_bot self.user_is_premium: Optional[bool] = user_is_premium self.max_quantity: Optional[int] = max_quantity + self.request_name: Optional[bool] = request_name + self.request_username: Optional[bool] = request_username + self.request_photo: Optional[bool] = request_photo self._id_attrs = (self.request_id,) @@ -138,6 +166,15 @@ class KeyboardButtonRequestChat(TelegramObject): applied. bot_is_member (:obj:`bool`, optional): Pass :obj:`True` to request a chat with the bot as a member. Otherwise, no additional restrictions are applied. + request_title (:obj:`bool`, optional): Pass :obj:`True` to request the chat's title. + + .. versionadded:: NEXT.VERSION + request_username (:obj:`bool`, optional): Pass :obj:`True` to request the chat's username. + + .. versionadded:: NEXT.VERSION + request_photo (:obj:`bool`, optional): Pass :obj:`True` to request the chat's photo. + + .. versionadded:: NEXT.VERSION Attributes: request_id (:obj:`int`): Identifier of the request. chat_is_channel (:obj:`bool`): Pass :obj:`True` to request a channel chat, pass @@ -145,7 +182,7 @@ class KeyboardButtonRequestChat(TelegramObject): chat_is_forum (:obj:`bool`): Optional. Pass :obj:`True` to request a forum supergroup, pass :obj:`False` to request a non-forum chat. If not specified, no additional restrictions are applied. - chat_has_username (:obj:`bool`, optional): Pass :obj:`True` to request a supergroup or a + chat_has_username (:obj:`bool`): Optional. Pass :obj:`True` to request a supergroup or a channel with a username, pass :obj:`False` to request a chat without a username. If not specified, no additional restrictions are applied. chat_is_created (:obj:`bool`) Optional. Pass :obj:`True` to request a chat owned by the @@ -159,6 +196,15 @@ class KeyboardButtonRequestChat(TelegramObject): applied. bot_is_member (:obj:`bool`) Optional. Pass :obj:`True` to request a chat with the bot as a member. Otherwise, no additional restrictions are applied. + request_title (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's title. + + .. versionadded:: NEXT.VERSION + request_username (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's username. + + .. versionadded:: NEXT.VERSION + request_photo (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's photo. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -170,6 +216,9 @@ class KeyboardButtonRequestChat(TelegramObject): "chat_is_forum", "request_id", "user_administrator_rights", + "request_title", + "request_username", + "request_photo", ) def __init__( @@ -182,6 +231,9 @@ def __init__( user_administrator_rights: Optional[ChatAdministratorRights] = None, bot_administrator_rights: Optional[ChatAdministratorRights] = None, bot_is_member: Optional[bool] = None, + request_title: Optional[bool] = None, + request_username: Optional[bool] = None, + request_photo: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -199,6 +251,9 @@ def __init__( ) self.bot_administrator_rights: Optional[ChatAdministratorRights] = bot_administrator_rights self.bot_is_member: Optional[bool] = bot_is_member + self.request_title: Optional[bool] = request_title + self.request_username: Optional[bool] = request_username + self.request_photo: Optional[bool] = request_photo self._id_attrs = (self.request_id,) diff --git a/telegram/_shared.py b/telegram/_shared.py index 89cb0b5d6a2..2bfc4764618 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -19,8 +19,17 @@ """This module contains two objects used for request chats/users service messages.""" from typing import Optional, Sequence, Tuple +from telegram._bot import Bot +from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict +from telegram._utils.warnings import warn +from telegram._utils.warnings_transition import ( + warn_about_deprecated_attr_in_property, + build_deprecation_warning_message, +) +from telegram.warnings import PTBDeprecationWarning class UsersShared(TelegramObject): @@ -37,40 +46,88 @@ class UsersShared(TelegramObject): Args: request_id (:obj:`int`): Identifier of the request. - user_ids (Sequence[:obj:`int`]): Identifiers of the shared users. These numbers may have + users (Sequence[:class:`telegram.SharedUser`]): Information about users shared with the + bot. Mutually exclusive with :paramref:`user_ids`. + + .. versionadded:: NEXT.VERSION + user_ids (Sequence[:obj:`int`], optional): Identifiers of the shared users. These numbers may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting them. But they have at most 52 significant bits, so 64-bit integers or double-precision float types are safe for storing these identifiers. The bot may not have access to the users and could be unable to use these identifiers, - unless the users are already known to the bot by some other means. + unless the users are already known to the bot by some other means. Mutually exclusive + with :paramref:`users`. + + .. versionchanged:: NEXT.VERSION + Bot API 7.2 introduced :paramref:`users` replacing this argument. PTB will + automatically convert this to that one, but for advanced options, please use + :paramref:`users` directly. + + .. deprecated:: NEXT.VERSION + In future versions, this argument will become keyword only. Attributes: request_id (:obj:`int`): Identifier of the request. - user_ids (Tuple[:obj:`int`]): Identifiers of the shared users. These numbers may have - more than 32 significant bits and some programming languages may have difficulty/silent - defects in interpreting them. But they have at most 52 significant bits, so 64-bit - integers or double-precision float types are safe for storing these identifiers. The - bot may not have access to the users and could be unable to use these identifiers, - unless the users are already known to the bot by some other means. + users (Sequence[:class:`telegram.SharedUser`]): Information about users shared with the + bot. Mutually exclusive with :attr:`user_ids`. + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("request_id", "user_ids") + __slots__ = ("request_id", "users") def __init__( self, request_id: int, - user_ids: Sequence[int], + users: Sequence[SharedUser], + user_ids: Optional[Sequence[int]] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.request_id: int = request_id - self.user_ids: Tuple[int, ...] = tuple(user_ids) + self.users: Tuple[SharedUser, ...] = users + self.user_ids: Optional[Tuple[int, ...]] = user_ids + + if user_ids is not None and users: + raise ValueError("`users` and `user_ids` are mutually exclusive") - self._id_attrs = (self.request_id, self.user_ids) + if user_ids is not None: + warn( + build_deprecation_warning_message( + deprecated_name="user_ids", + new_name="users", + object_type="parameter", + bot_api_version="7.2", + ), + PTBDeprecationWarning, + stacklevel=2, + ) + + self._id_attrs = (self.request_id, self.users) self._freeze() + @property + def user_ids(self) -> Optional[Tuple[int, ...]]: + """ + Optional[Tuple[:obj:`int`]]: Identifiers of the shared users. These numbers may have + more than 32 significant bits and some programming languages may have difficulty/silent + defects in interpreting them. But they have at most 52 significant bits, so 64-bit + integers or double-precision float types are safe for storing these identifiers. The + bot may not have access to the users and could be unable to use these identifiers, + unless the users are already known to the bot by some other means. + + .. deprecated:: NEXT.VERSION + """ + warn_about_deprecated_attr_in_property( + deprecated_attr_name="user_ids", + new_attr_name="users", + bot_api_version="7.2", + stacklevel=2, + ) + return tuple(user.user_id for user in self.users) + class ChatShared(TelegramObject): """ @@ -88,6 +145,17 @@ class ChatShared(TelegramObject): bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64-bit integer or double-precision float type are safe for storing this identifier. + title (:obj:`str`, optional): Title of the chat, if the title was requested by the bot. + + .. versionadded:: NEXT.VERSION + username (:obj:`str`, optional): Username of the chat, if the username was requested by + the bot and available. + + .. versionadded:: NEXT.VERSION + photo (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the chat photo, + if the photo was requested by the bot + + .. versionadded:: NEXT.VERSION Attributes: request_id (:obj:`int`): Identifier of the request. @@ -95,21 +163,121 @@ class ChatShared(TelegramObject): bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64-bit integer or double-precision float type are safe for storing this identifier. + title (:obj:`str`): Optional. Title of the chat, if the title was requested by the bot. + + .. versionadded:: NEXT.VERSION + username (:obj:`str`): Optional. Username of the chat, if the username was requested by + the bot and available. + + .. versionadded:: NEXT.VERSION + photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Available sizes of the chat photo, + if the photo was requested by the bot + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("chat_id", "request_id") + __slots__ = ("chat_id", "request_id", "title", "username", "photo") def __init__( self, request_id: int, chat_id: int, + title: Optional[str] = None, + username: Optional[str] = None, + photo: Optional[Sequence[PhotoSize]] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.request_id: int = request_id self.chat_id: int = chat_id + self.title: Optional[str] = title + self.username: Optional[str] = username + self.photo: Optional[Tuple[PhotoSize, ...]] = parse_sequence_arg(photo) self._id_attrs = (self.request_id, self.chat_id) self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatShared"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["photo"] = PhotoSize.de_list(data.get("photo"), bot) + return super().de_json(data=data, bot=bot) + + +class SharedUser(TelegramObject): + """ + This object contains information about a user that was shared with the bot using a + :class:`telegram.KeyboardButtonRequestUser` button. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`user_id` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): Identifier of the shared user. This number may have 32 significant + bits and some programming languages may have difficulty/silent defects in interpreting + it. But it has atmost 52 significant bits, so 64-bit integers or double-precision + float types are safe for storing these identifiers. The bot may not have access to the + user and could be unable to use this identifier, unless the user is already known to the + bot by some other means. + first_name (:obj:`str`, optional): First name of the user, if the name was requested by the bot. + last_name (:obj:`str`, optional): Last name of the user, if the name was requested by the bot. + username (:obj:`str`, optional): Username of the user, if the username was requested by the bot. + photo (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the chat photo, if + the photo was requested by the bot. + + Attributes: + user_id (:obj:`int`): Identifier of the shared user. This number may have 32 significant + bits and some programming languages may have difficulty/silent defects in interpreting + it. But it has atmost 52 significant bits, so 64-bit integers or double-precision + float types are safe for storing these identifiers. The bot may not have access to the + user and could be unable to use this identifier, unless the user is already known to the + bot by some other means. + first_name (:obj:`str`): Optional. First name of the user, if the name was requested by the bot. + last_name (:obj:`str`): Optional. Last name of the user, if the name was requested by the bot. + username (:obj:`str`): Optional. Username of the user, if the username was requested by the bot. + photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Available sizes of the chat photo, if + the photo was requested by the bot. + """ + + __slots__ = ("user_id", "first_name", "last_name", "username", "photo") + + def __init__( + self, + ruser_id: int, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + username: Optional[str] = None, + photo: Optional[Sequence[PhotoSize]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.user_id: int = user_id + self.first_name: Optional[str] = first_name + self.last_name: Optional[str] = title + self.username: Optional[str] = username + self.photo: Optional[Tuple[PhotoSize, ...]] = parse_sequence_arg(photo) + + self._id_attrs = self.user_id + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SharedUser"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["photo"] = PhotoSize.de_list(data.get("photo"), bot) + return super().de_json(data=data, bot=bot) From 6935e6aa3ca84602bb5daca818c23e70834d0212 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sun, 7 Apr 2024 08:34:03 +0530 Subject: [PATCH 26/30] fix pre-commit --- telegram/_keyboardbuttonrequest.py | 23 ++++++---- telegram/_shared.py | 73 +++++++++++++++++------------- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/telegram/_keyboardbuttonrequest.py b/telegram/_keyboardbuttonrequest.py index 10f80cd65da..bd12d27ad09 100644 --- a/telegram/_keyboardbuttonrequest.py +++ b/telegram/_keyboardbuttonrequest.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains two objects to request chats/users.""" + from typing import TYPE_CHECKING, Optional from telegram._chatadministratorrights import ChatAdministratorRights @@ -56,7 +57,8 @@ class KeyboardButtonRequestUsers(TelegramObject): . .. versionadded:: 20.8 - request_name (:obj:`bool`, optional): Pass :obj:`True` to request the users' first and last name. + request_name (:obj:`bool`, optional): Pass :obj:`True` to request the users' first and last + name. .. versionadded:: NEXT.VERSION request_username (:obj:`bool`, optional): Pass :obj:`True` to request the users' username. @@ -80,7 +82,8 @@ class KeyboardButtonRequestUsers(TelegramObject): . .. versionadded:: 20.8 - request_name (:obj:`bool`): Optional. Pass :obj:`True` to request the users' first and last name. + request_name (:obj:`bool`): Optional. Pass :obj:`True` to request the users' first and last + name. .. versionadded:: NEXT.VERSION request_username (:obj:`bool`): Optional. Pass :obj:`True` to request the users' username. @@ -95,11 +98,11 @@ class KeyboardButtonRequestUsers(TelegramObject): __slots__ = ( "max_quantity", "request_id", - "user_is_bot", - "user_is_premium", "request_name", - "request_username", "request_photo", + "request_username", + "user_is_bot", + "user_is_premium", ) def __init__( @@ -108,9 +111,9 @@ def __init__( user_is_bot: Optional[bool] = None, user_is_premium: Optional[bool] = None, max_quantity: Optional[int] = None, - request_name: Optional[bool]= None, - request_username: Optional[bool]= None, - request_photo: Optional[bool]= None, + request_name: Optional[bool] = None, + request_username: Optional[bool] = None, + request_photo: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -215,10 +218,10 @@ class KeyboardButtonRequestChat(TelegramObject): "chat_is_created", "chat_is_forum", "request_id", - "user_administrator_rights", + "request_photo", "request_title", "request_username", - "request_photo", + "user_administrator_rights", ) def __init__( diff --git a/telegram/_shared.py b/telegram/_shared.py index 2bfc4764618..cf5f66193a6 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -17,20 +17,22 @@ # 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 two objects used for request chats/users service messages.""" -from typing import Optional, Sequence, Tuple +from typing import TYPE_CHECKING, Optional, Sequence, Tuple -from telegram._bot import Bot from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict from telegram._utils.warnings import warn from telegram._utils.warnings_transition import ( - warn_about_deprecated_attr_in_property, build_deprecation_warning_message, + warn_about_deprecated_attr_in_property, ) from telegram.warnings import PTBDeprecationWarning +if TYPE_CHECKING: + from telegram._bot import Bot + class UsersShared(TelegramObject): """ @@ -50,13 +52,13 @@ class UsersShared(TelegramObject): bot. Mutually exclusive with :paramref:`user_ids`. .. versionadded:: NEXT.VERSION - user_ids (Sequence[:obj:`int`], optional): Identifiers of the shared users. These numbers may have - more than 32 significant bits and some programming languages may have difficulty/silent - defects in interpreting them. But they have at most 52 significant bits, so 64-bit - integers or double-precision float types are safe for storing these identifiers. The - bot may not have access to the users and could be unable to use these identifiers, - unless the users are already known to the bot by some other means. Mutually exclusive - with :paramref:`users`. + user_ids (Sequence[:obj:`int`], optional): Identifiers of the shared users. These numbers + may have more than 32 significant bits and some programming languages may have + difficulty/silent defects in interpreting them. But they have at most 52 significant + bits, so 64-bit integers or double-precision float types are safe for storing these + identifiers. The bot may not have access to the users and could be unable to use + these identifiers, unless the users are already known to the bot by some other means. + Mutually exclusive with :paramref:`users`. .. versionchanged:: NEXT.VERSION Bot API 7.2 introduced :paramref:`users` replacing this argument. PTB will @@ -68,7 +70,7 @@ class UsersShared(TelegramObject): Attributes: request_id (:obj:`int`): Identifier of the request. - users (Sequence[:class:`telegram.SharedUser`]): Information about users shared with the + users (Tuple[:class:`telegram.SharedUser`]): Information about users shared with the bot. Mutually exclusive with :attr:`user_ids`. .. versionadded:: NEXT.VERSION @@ -79,16 +81,17 @@ class UsersShared(TelegramObject): def __init__( self, request_id: int, - users: Sequence[SharedUser], + users: Sequence["SharedUser"], user_ids: Optional[Sequence[int]] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.request_id: int = request_id - self.users: Tuple[SharedUser, ...] = users - self.user_ids: Optional[Tuple[int, ...]] = user_ids - + self.users: Tuple[SharedUser, ...] = parse_sequence_arg(users) + self.user_ids: Optional[Tuple[int, ...]] = parse_sequence_arg( # type: ignore[misc] + user_ids + ) if user_ids is not None and users: raise ValueError("`users` and `user_ids` are mutually exclusive") @@ -176,7 +179,7 @@ class ChatShared(TelegramObject): .. versionadded:: NEXT.VERSION """ - __slots__ = ("chat_id", "request_id", "title", "username", "photo") + __slots__ = ("chat_id", "photo", "request_id", "title", "username") def __init__( self, @@ -226,33 +229,39 @@ class SharedUser(TelegramObject): bits and some programming languages may have difficulty/silent defects in interpreting it. But it has atmost 52 significant bits, so 64-bit integers or double-precision float types are safe for storing these identifiers. The bot may not have access to the - user and could be unable to use this identifier, unless the user is already known to the - bot by some other means. - first_name (:obj:`str`, optional): First name of the user, if the name was requested by the bot. - last_name (:obj:`str`, optional): Last name of the user, if the name was requested by the bot. - username (:obj:`str`, optional): Username of the user, if the username was requested by the bot. - photo (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the chat photo, if - the photo was requested by the bot. + user and could be unable to use this identifier, unless the user is already known to + the bot by some other means. + first_name (:obj:`str`, optional): First name of the user, if the name was requested by the + bot. + last_name (:obj:`str`, optional): Last name of the user, if the name was requested by the + bot. + username (:obj:`str`, optional): Username of the user, if the username was requested by the + bot. + photo (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the chat photo, + if the photo was requested by the bot. Attributes: user_id (:obj:`int`): Identifier of the shared user. This number may have 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has atmost 52 significant bits, so 64-bit integers or double-precision float types are safe for storing these identifiers. The bot may not have access to the - user and could be unable to use this identifier, unless the user is already known to the - bot by some other means. - first_name (:obj:`str`): Optional. First name of the user, if the name was requested by the bot. - last_name (:obj:`str`): Optional. Last name of the user, if the name was requested by the bot. - username (:obj:`str`): Optional. Username of the user, if the username was requested by the bot. + user and could be unable to use this identifier, unless the user is already known to + the bot by some other means. + first_name (:obj:`str`): Optional. First name of the user, if the name was requested by the + bot. + last_name (:obj:`str`): Optional. Last name of the user, if the name was requested by the + bot. + username (:obj:`str`): Optional. Username of the user, if the username was requested by the + bot. photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Available sizes of the chat photo, if the photo was requested by the bot. """ - __slots__ = ("user_id", "first_name", "last_name", "username", "photo") + __slots__ = ("first_name", "last_name", "photo", "user_id", "username") def __init__( self, - ruser_id: int, + user_id: int, first_name: Optional[str] = None, last_name: Optional[str] = None, username: Optional[str] = None, @@ -263,11 +272,11 @@ def __init__( super().__init__(api_kwargs=api_kwargs) self.user_id: int = user_id self.first_name: Optional[str] = first_name - self.last_name: Optional[str] = title + self.last_name: Optional[str] = last_name self.username: Optional[str] = username self.photo: Optional[Tuple[PhotoSize, ...]] = parse_sequence_arg(photo) - self._id_attrs = self.user_id + self._id_attrs = (self.user_id,) self._freeze() From 91b1da637fe6da8a94b17f87da6fabc59134d071 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 7 Apr 2024 06:00:59 -0400 Subject: [PATCH 27/30] Review: remove business_connection_id from Message methods also doc fixes and other stuff --- telegram/_birthdate.py | 18 ++++ telegram/_bot.py | 2 + telegram/_business.py | 4 +- telegram/_files/sticker.py | 41 ++++++++- telegram/_message.py | 84 ++++++++++--------- telegram/_update.py | 12 +++ .../_handlers/businessconnectionhandler.py | 6 +- .../businessmessagesdeletedhandler.py | 4 +- telegram/ext/filters.py | 5 ++ tests/_files/test_sticker.py | 5 ++ tests/test_birthdate.py | 13 +++ tests/test_bot.py | 30 +++++-- tests/test_official/exceptions.py | 1 + 13 files changed, 168 insertions(+), 57 deletions(-) diff --git a/telegram/_birthdate.py b/telegram/_birthdate.py index 6a58b3072b0..d47a037a31e 100644 --- a/telegram/_birthdate.py +++ b/telegram/_birthdate.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Birthday.""" +from datetime import datetime from typing import Optional from telegram._telegramobject import TelegramObject @@ -68,3 +69,20 @@ def __init__( ) self._freeze() + + def to_date(self, year: Optional[int] = None) -> datetime: + """Return the birthdate as a datetime object. + + Args: + year (:obj:`int`, optional): The year to use. Required, if the :attr:`year` was not + present. + + Returns: + :obj:`datetime.datetime`: The birthdate as a datetime object. + """ + if self.year is None and year is None: + raise ValueError( + "The `year` argument is required if the `year` attribute was not present." + ) + + return datetime(year or self.year, self.month, self.day) # type: ignore[arg-type] diff --git a/telegram/_bot.py b/telegram/_bot.py index d4099992fac..f9682f58b86 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -964,6 +964,8 @@ async def send_message( .. versionadded:: 20.8 business_connection_id (:obj:`str`, optional): |business_id_str| + .. versionadded:: NEXT.VERSION + Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience diff --git a/telegram/_business.py b/telegram/_business.py index d208de31092..8d036a2cdbc 100644 --- a/telegram/_business.py +++ b/telegram/_business.py @@ -143,7 +143,7 @@ class BusinessMessagesDeleted(TelegramObject): business_connection_id (:obj:`str`): Unique identifier of the business connection. chat (:class:`telegram.Chat`): Information about a chat in the business account. The bot may not have access to the chat or the corresponding user. - message_ids (Sequence[:obj:`int`]): A list of identifiers of the deleted messages in the + message_ids (Tuple[:obj:`int`]): A list of identifiers of the deleted messages in the chat of the business account. """ @@ -164,7 +164,7 @@ def __init__( super().__init__(api_kwargs=api_kwargs) self.business_connection_id: str = business_connection_id self.chat: Chat = chat - self.message_ids: Sequence[int] = parse_sequence_arg(message_ids) + self.message_ids: Tuple[int, ...] = parse_sequence_arg(message_ids) self._id_attrs = ( self.business_connection_id, diff --git a/telegram/_files/sticker.py b/telegram/_files/sticker.py index f6fcc6049dc..e939b15978d 100644 --- a/telegram/_files/sticker.py +++ b/telegram/_files/sticker.py @@ -27,6 +27,8 @@ from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict +from telegram._utils.warnings import warn +from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Bot @@ -229,7 +231,8 @@ class StickerSet(TelegramObject): .. versionchanged:: NEXT.VERSION - The parameter ``is_video`` and ``is_animated`` has been removed. + The parameters ``is_video`` and ``is_animated`` are deprecated and now made optional. Thus, + the order of the arguments had to be changed. .. versionchanged:: 20.5 |removed_thumb_note| @@ -237,6 +240,17 @@ class StickerSet(TelegramObject): Args: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. + is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers. + + .. deprecated:: NEXT.VERSION + Bot API 7.2 deprecated this field. This parameter will be removed in a future + version of the library. + is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. + .. versionadded:: 13.11 + + .. deprecated:: NEXT.VERSION + Bot API 7.2 deprecated this field. This parameter will be removed in a future + version of the library. stickers (Sequence[:class:`telegram.Sticker`]): List of all set stickers. .. versionchanged:: 20.0 @@ -255,6 +269,17 @@ class StickerSet(TelegramObject): Attributes: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. + is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers. + + .. deprecated:: NEXT.VERSION + Bot API 7.2 deprecated this field. This parameter will be removed in a future + version of the library. + is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. + .. versionadded:: 13.11 + + .. deprecated:: NEXT.VERSION + Bot API 7.2 deprecated this field. This parameter will be removed in a future + version of the library. stickers (Tuple[:class:`telegram.Sticker`]): List of all set stickers. .. versionchanged:: 20.0 @@ -272,6 +297,8 @@ class StickerSet(TelegramObject): """ __slots__ = ( + "is_animated", + "is_video", "name", "sticker_type", "stickers", @@ -285,6 +312,8 @@ def __init__( title: str, stickers: Sequence[Sticker], sticker_type: str, + is_animated: Optional[bool] = None, + is_video: Optional[bool] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, @@ -295,8 +324,16 @@ def __init__( self.stickers: Tuple[Sticker, ...] = parse_sequence_arg(stickers) self.sticker_type: str = sticker_type # Optional - self.thumbnail: Optional[PhotoSize] = thumbnail + if is_animated is not None or is_video is not None: + warn( + "The parameters `is_animated` and `is_video` are deprecated and will be removed " + "in a future version.", + PTBDeprecationWarning, + stacklevel=2, + ) + self.is_animated: Optional[bool] = is_animated + self.is_video: Optional[bool] = is_video self._id_attrs = (self.name,) self._freeze() diff --git a/telegram/_message.py b/telegram/_message.py index be195fd80dd..e2a71b7155f 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -305,6 +305,8 @@ class Message(MaybeInaccessibleMessage): is_from_offline (:obj:`bool`, optional): :obj:`True`, if the message was sent by an implicit action, for example, as an away or a greeting business message, or as a scheduled message. + + .. versionadded:: NEXT.VERSION media_group_id (:obj:`str`, optional): The unique identifier of a media message group this message belongs to. text (:obj:`str`, optional): For text messages, the actual UTF-8 text of the message, @@ -587,6 +589,8 @@ class Message(MaybeInaccessibleMessage): is_from_offline (:obj:`bool`): Optional. :obj:`True`, if the message was sent by an implicit action, for example, as an away or a greeting business message, or as a scheduled message. + + .. versionadded:: NEXT.VERSION media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this message belongs to. text (:obj:`str`): Optional. For text messages, the actual UTF-8 text of the message, @@ -1600,7 +1604,6 @@ async def reply_text( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1618,6 +1621,7 @@ async def reply_text( await bot.send_message( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -1663,7 +1667,7 @@ async def reply_text( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_markdown( @@ -1676,7 +1680,6 @@ async def reply_markdown( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1695,6 +1698,7 @@ async def reply_markdown( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, parse_mode=ParseMode.MARKDOWN, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -1745,7 +1749,7 @@ async def reply_markdown( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_markdown_v2( @@ -1758,7 +1762,6 @@ async def reply_markdown_v2( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1777,6 +1780,7 @@ async def reply_markdown_v2( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, parse_mode=ParseMode.MARKDOWN_V2, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -1823,7 +1827,7 @@ async def reply_markdown_v2( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_html( @@ -1836,7 +1840,6 @@ async def reply_html( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1855,6 +1858,7 @@ async def reply_html( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, parse_mode=ParseMode.HTML, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -1901,7 +1905,7 @@ async def reply_html( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_media_group( @@ -1913,7 +1917,6 @@ async def reply_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1933,6 +1936,7 @@ async def reply_media_group( await bot.send_media_group( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -1978,7 +1982,7 @@ async def reply_media_group( caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_photo( @@ -1993,7 +1997,6 @@ async def reply_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2011,6 +2014,7 @@ async def reply_photo( await bot.send_photo( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2057,7 +2061,7 @@ async def reply_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, has_spoiler=has_spoiler, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_audio( @@ -2075,7 +2079,6 @@ async def reply_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2093,6 +2096,7 @@ async def reply_audio( await bot.send_audio( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2142,7 +2146,7 @@ async def reply_audio( pool_timeout=pool_timeout, api_kwargs=api_kwargs, thumbnail=thumbnail, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_document( @@ -2158,7 +2162,6 @@ async def reply_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2176,6 +2179,7 @@ async def reply_document( await bot.send_document( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2223,7 +2227,7 @@ async def reply_document( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_animation( @@ -2242,7 +2246,6 @@ async def reply_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2260,6 +2263,7 @@ async def reply_animation( await bot.send_animation( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2310,7 +2314,7 @@ async def reply_animation( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_sticker( @@ -2322,7 +2326,6 @@ async def reply_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2339,6 +2342,7 @@ async def reply_sticker( await bot.send_sticker( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2381,7 +2385,7 @@ async def reply_sticker( protect_content=protect_content, message_thread_id=message_thread_id, emoji=emoji, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_video( @@ -2401,7 +2405,6 @@ async def reply_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2419,6 +2422,7 @@ async def reply_video( await bot.send_video( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2470,7 +2474,7 @@ async def reply_video( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_video_note( @@ -2484,7 +2488,6 @@ async def reply_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2502,6 +2505,7 @@ async def reply_video_note( await bot.send_video_note( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2547,7 +2551,7 @@ async def reply_video_note( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_voice( @@ -2562,7 +2566,6 @@ async def reply_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2580,6 +2583,7 @@ async def reply_voice( await bot.send_voice( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2626,7 +2630,7 @@ async def reply_voice( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_location( @@ -2642,7 +2646,6 @@ async def reply_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2660,6 +2663,7 @@ async def reply_location( await bot.send_location( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2707,7 +2711,7 @@ async def reply_location( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_venue( @@ -2725,7 +2729,6 @@ async def reply_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2743,6 +2746,7 @@ async def reply_venue( await bot.send_venue( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2792,7 +2796,7 @@ async def reply_venue( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_contact( @@ -2806,7 +2810,6 @@ async def reply_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2824,6 +2827,7 @@ async def reply_contact( await bot.send_contact( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2869,7 +2873,7 @@ async def reply_contact( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_poll( @@ -2891,7 +2895,6 @@ async def reply_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2908,6 +2911,7 @@ async def reply_poll( await bot.send_poll( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -2960,7 +2964,7 @@ async def reply_poll( explanation_entities=explanation_entities, protect_content=protect_content, message_thread_id=message_thread_id, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_dice( @@ -2971,7 +2975,6 @@ async def reply_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2988,6 +2991,7 @@ async def reply_dice( await bot.send_dice( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -3029,14 +3033,13 @@ async def reply_dice( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_chat_action( self, action: str, message_thread_id: Optional[int] = None, - business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3049,6 +3052,7 @@ async def reply_chat_action( await bot.send_chat_action( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -3073,7 +3077,7 @@ async def reply_chat_action( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_game( @@ -3084,7 +3088,6 @@ async def reply_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, - business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3101,6 +3104,7 @@ async def reply_game( await bot.send_game( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, *args, **kwargs, ) @@ -3144,7 +3148,7 @@ async def reply_game( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, - business_connection_id=business_connection_id, + business_connection_id=self.business_connection_id, ) async def reply_invoice( diff --git a/telegram/_update.py b/telegram/_update.py index 57fb34e0344..d7dc727b86e 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -471,6 +471,10 @@ def effective_user(self) -> Optional["User"]: is present. + .. versionchanged:: NEXT.VERSION + This property now also considers :attr:`business_connection`, :attr:`business_message` + and :attr:`edited_business_message`. + Example: * If :attr:`message` is present, this will give :attr:`telegram.Message.from_user`. @@ -599,6 +603,10 @@ def effective_chat(self) -> Optional["Chat"]: :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, :attr:`poll_answer`, or :attr:`business_connection` is present. + .. versionchanged:: NEXT.VERSION + This property now also considers :attr:`business_message`, + :attr:`edited_business_message`, and :attr:`deleted_business_messages`. + Example: If :attr:`message` is present, this will give :attr:`telegram.Message.chat`. @@ -665,6 +673,10 @@ def effective_message(self) -> Optional[Message]: :attr:`callback_query` (i.e. :attr:`telegram.CallbackQuery.message`) or :obj:`None`, if none of those are present. + .. versionchanged:: NEXT.VERSION + This property now also considers :attr:`business_message`, and + :attr:`edited_business_message`. + Tip: This property will only ever return objects of type :class:`telegram.Message` or :obj:`None`, never :class:`telegram.MaybeInaccessibleMessage` or diff --git a/telegram/ext/_handlers/businessconnectionhandler.py b/telegram/ext/_handlers/businessconnectionhandler.py index a6f2cc01923..21336dceff8 100644 --- a/telegram/ext/_handlers/businessconnectionhandler.py +++ b/telegram/ext/_handlers/businessconnectionhandler.py @@ -42,7 +42,7 @@ class BusinessConnectionHandler(BaseHandler[Update, CCT]): async def callback(update: Update, context: CallbackContext) user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only - those are from the specified user ID(s). + those which are from the specified user ID(s). username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only those which are from the specified username(s). @@ -91,7 +91,5 @@ def check_update(self, update: object) -> bool: return True if update.business_connection.user.id in self._user_ids: return True - if update.business_connection.user.username in self._usernames: - return True - return False + return update.business_connection.user.username in self._usernames return False diff --git a/telegram/ext/_handlers/businessmessagesdeletedhandler.py b/telegram/ext/_handlers/businessmessagesdeletedhandler.py index 9dabdf2ac60..14fed0b5a88 100644 --- a/telegram/ext/_handlers/businessmessagesdeletedhandler.py +++ b/telegram/ext/_handlers/businessmessagesdeletedhandler.py @@ -91,7 +91,5 @@ def check_update(self, update: object) -> bool: return True if update.deleted_business_messages.chat.id in self._chat_ids: return True - if update.deleted_business_messages.chat.username in self._usernames: - return True - return False + return update.deleted_business_messages.chat.username in self._usernames return False diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index e2418f0d119..961ba9bf228 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -273,6 +273,11 @@ def name(self, name: str) -> None: def check_update(self, update: Update) -> Optional[Union[bool, FilterDataDict]]: """Checks if the specified update should be handled by this filter. + .. versionchanged:: NEXT.VERSION + This filter now also returns :obj:`True` if the update contains + :attr:`~telegram.Update.business_message` + or :attr:`~telegram.Update.edited_business_message`. + Args: update (:class:`telegram.Update`): The update to check. diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index 6013533424c..c408468118a 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -800,6 +800,11 @@ async def make_assertion(*_, **kwargs): sticker_format="static", ) + async def test_deprecation_creation_args(self, recwarn): + with pytest.warns(PTBDeprecationWarning, match="The parameters `is_animated` and ") as w: + StickerSet("name", "title", [], "static", is_animated=True) + assert w[0].filename == __file__, "wrong stacklevel!" + @pytest.mark.xdist_group("stickerset") class TestStickerSetWithRequest: diff --git a/tests/test_birthdate.py b/tests/test_birthdate.py index 096c0ba61a6..4c028661ac8 100644 --- a/tests/test_birthdate.py +++ b/tests/test_birthdate.py @@ -16,6 +16,8 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +from datetime import datetime + import pytest from telegram import Birthdate @@ -68,3 +70,14 @@ def test_equality(self): assert bd1 != bd4 assert hash(bd1) != hash(bd4) + + def test_to_date(self, birthdate): + assert isinstance(birthdate.to_date(), datetime) + assert birthdate.to_date() == datetime(self.year, self.month, self.day) + new_bd = birthdate.to_date(2023) + assert new_bd == datetime(2023, self.month, self.day) + + def test_to_date_no_year(self): + bd = Birthdate(1, 1) + with pytest.raises(ValueError, match="The `year` argument is required"): + bd.to_date() diff --git a/tests/test_bot.py b/tests/test_bot.py index 32a82bdf428..7021867da64 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -39,6 +39,7 @@ BotDescription, BotName, BotShortDescription, + BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -2088,7 +2089,9 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): ) async def test_business_connection_id_argument(self, bot, monkeypatch): - """We can't connect to a business acc, so we just test that the correct data is passed.""" + """We can't connect to a business acc, so we just test that the correct data is passed. + We also can't test every single method easily, so we just test one. Our linting will catch + any unused args with the others.""" async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.parameters.get("business_connection_id") == 42 @@ -2096,6 +2099,26 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_message(2, "text", business_connection_id=42) + async def test_get_business_connection(self, bot, monkeypatch): + bci = "42" + user = User(1, "first", False) + user_chat_id = 1 + date = dtm.datetime.utcnow() + can_reply = True + is_enabled = True + bc = BusinessConnection(bci, user, user_chat_id, date, can_reply, is_enabled).to_json() + + async def do_request(*args, **kwargs): + data = kwargs.get("request_data") + obj = data.parameters.get("business_connection_id") + if obj == bci: + return 200, f'{{"ok": true, "result": {bc}}}'.encode() + return 400, b'{"ok": false, "result": []}' + + monkeypatch.setattr(bot.request, "do_request", do_request) + obj = await bot.get_business_connection(business_connection_id=bci) + assert isinstance(obj, BusinessConnection) + class TestBotWithRequest: """ @@ -2890,11 +2913,6 @@ async def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot, use_ip assert info.ip_address is None assert info.has_custom_certificate is False - async def test_get_business_connection(self, bot): - # TODO: Get a business connection and test this properly - pass - # assert await bot.get_business_connection("TEST") is None - async def test_leave_chat(self, bot): with pytest.raises(BadRequest, match="Chat not found"): await bot.leave_chat(-123456) diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index f2c81d7732c..19b41fa517f 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -168,6 +168,7 @@ def ignored_param_requirements(object_name: str) -> set[str]: # Arguments that are optional arguments for now for backwards compatibility BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = { "create_new_sticker_set": {"sticker_format"}, # removed by bot api 7.2 + "StickerSet": {"is_animated", "is_video"}, # removed by bot api 7.2 } From 8addbf4cd45769e7a16d584ef913ddf795415d33 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:36:46 +0200 Subject: [PATCH 28/30] Work on UsersShared --- telegram/_shared.py | 50 ++++++++++++++++++++++++++++---------------- tests/test_shared.py | 38 ++++++++++++++++++++++++--------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/telegram/_shared.py b/telegram/_shared.py index cf5f66193a6..ca188ed4e05 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -40,38 +40,40 @@ class UsersShared(TelegramObject): using a :class:`telegram.KeyboardButtonRequestUsers` button. Objects of this class are comparable in terms of equality. Two objects of this class are - considered equal, if their :attr:`request_id` and :attr:`user_ids` are equal. + considered equal, if their :attr:`request_id` and :attr:`users` are equal. .. versionadded:: 20.8 Bot API 7.0 replaces ``UserShared`` with this class. The only difference is that now the :attr:`user_ids` is a sequence instead of a single integer. + .. versionchanged:: NEXT.VERSION + The argument :attr:`users` is now considered for the equality comparison instead of + :attr:`user_ids`. + Args: request_id (:obj:`int`): Identifier of the request. users (Sequence[:class:`telegram.SharedUser`]): Information about users shared with the - bot. Mutually exclusive with :paramref:`user_ids`. + bot. .. versionadded:: NEXT.VERSION + + .. deprecated:: NEXT.VERSION + In future versions, this argument will become keyword only. user_ids (Sequence[:obj:`int`], optional): Identifiers of the shared users. These numbers may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting them. But they have at most 52 significant bits, so 64-bit integers or double-precision float types are safe for storing these identifiers. The bot may not have access to the users and could be unable to use these identifiers, unless the users are already known to the bot by some other means. - Mutually exclusive with :paramref:`users`. - - .. versionchanged:: NEXT.VERSION - Bot API 7.2 introduced :paramref:`users` replacing this argument. PTB will - automatically convert this to that one, but for advanced options, please use - :paramref:`users` directly. .. deprecated:: NEXT.VERSION - In future versions, this argument will become keyword only. + Bot API 7.2 introduced by :paramref:`users`, replacing this argument. Hence, this + argument is now optional and will be removed in future versions. Attributes: request_id (:obj:`int`): Identifier of the request. users (Tuple[:class:`telegram.SharedUser`]): Information about users shared with the - bot. Mutually exclusive with :attr:`user_ids`. + bot. .. versionadded:: NEXT.VERSION """ @@ -81,19 +83,18 @@ class UsersShared(TelegramObject): def __init__( self, request_id: int, - users: Sequence["SharedUser"], user_ids: Optional[Sequence[int]] = None, + users: Optional[Sequence["SharedUser"]] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.request_id: int = request_id + + if users is None: + raise TypeError("`users` is a required argument since Bot API 7.2") + self.users: Tuple[SharedUser, ...] = parse_sequence_arg(users) - self.user_ids: Optional[Tuple[int, ...]] = parse_sequence_arg( # type: ignore[misc] - user_ids - ) - if user_ids is not None and users: - raise ValueError("`users` and `user_ids` are mutually exclusive") if user_ids is not None: warn( @@ -111,10 +112,21 @@ def __init__( self._freeze() + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["UsersShared"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["users"] = SharedUser.de_list(data.get("users"), bot) + return super().de_json(data=data, bot=bot) + @property - def user_ids(self) -> Optional[Tuple[int, ...]]: + def user_ids(self) -> Tuple[int, ...]: """ - Optional[Tuple[:obj:`int`]]: Identifiers of the shared users. These numbers may have + Tuple[:obj:`int`]: Identifiers of the shared users. These numbers may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting them. But they have at most 52 significant bits, so 64-bit integers or double-precision float types are safe for storing these identifiers. The @@ -122,6 +134,8 @@ def user_ids(self) -> Optional[Tuple[int, ...]]: unless the users are already known to the bot by some other means. .. deprecated:: NEXT.VERSION + As Bot API 7.2 replaces this attribute with :attr:`users`, this attribute will be + removed in future versions. """ warn_about_deprecated_attr_in_property( deprecated_attr_name="user_ids", diff --git a/tests/test_shared.py b/tests/test_shared.py index 3c76eb329f5..f12f51abb81 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -19,19 +19,20 @@ import pytest -from telegram import ChatShared, UsersShared +from telegram import ChatShared, SharedUser, UsersShared +from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @pytest.fixture(scope="class") def users_shared(): - return UsersShared(TestUsersSharedBase.request_id, TestUsersSharedBase.user_ids) + return UsersShared(TestUsersSharedBase.request_id, users=TestUsersSharedBase.users) class TestUsersSharedBase: request_id = 789 - user_id = 101112 - user_ids = (user_id, 101113) + user_ids = (101112, 101113) + users = (SharedUser(101112, "user1"), SharedUser(101113, "user2")) class TestUsersSharedWithoutRequest(TestUsersSharedBase): @@ -45,24 +46,41 @@ def test_to_dict(self, users_shared): assert isinstance(users_shared_dict, dict) assert users_shared_dict["request_id"] == self.request_id - assert users_shared_dict["user_ids"] == list(self.user_ids) + assert users_shared_dict["users"] == [user.to_dict() for user in self.users] def test_de_json(self, bot): json_dict = { "request_id": self.request_id, - "user_ids": self.user_ids, + "users": [user.to_dict() for user in self.users], } users_shared = UsersShared.de_json(json_dict, bot) assert users_shared.api_kwargs == {} assert users_shared.request_id == self.request_id + assert users_shared.users == self.users assert users_shared.user_ids == tuple(self.user_ids) + assert UsersShared.de_json({}, bot) is None + + def test_users_is_required_argument(self): + with pytest.raises(TypeError, match="`users` is a required argument"): + UsersShared(self.request_id, user_ids=self.user_ids) + + def test_user_ids_deprecation_warning(self): + with pytest.warns( + PTBDeprecationWarning, match="'user_ids' was renamed to 'users' in Bot API 7.2" + ): + users_shared = UsersShared(self.request_id, user_ids=self.user_ids, users=self.users) + with pytest.warns( + PTBDeprecationWarning, match="renamed the attribute 'user_ids' to 'users'" + ): + users_shared.user_ids + def test_equality(self): - a = UsersShared(self.request_id, self.user_ids) - b = UsersShared(self.request_id, self.user_ids) - c = UsersShared(1, self.user_ids) - d = UsersShared(self.request_id, [1, 2]) + a = UsersShared(self.request_id, users=self.users) + b = UsersShared(self.request_id, users=self.users) + c = UsersShared(1, users=self.users) + d = UsersShared(self.request_id, users=(SharedUser(1, "user1"), SharedUser(1, "user2"))) assert a == b assert hash(a) == hash(b) From b1af7d5a65c5d52b558eade4410927b706a46694 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 8 Apr 2024 21:20:37 +0200 Subject: [PATCH 29/30] Work on tests --- telegram/_shared.py | 9 ++- tests/test_message.py | 91 ++++++++++++++++++++++--------- tests/test_official/exceptions.py | 1 + tests/test_shared.py | 3 +- 4 files changed, 75 insertions(+), 29 deletions(-) diff --git a/telegram/_shared.py b/telegram/_shared.py index ca188ed4e05..ee34691743b 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -121,7 +121,14 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["UsersShared" return None data["users"] = SharedUser.de_list(data.get("users"), bot) - return super().de_json(data=data, bot=bot) + + api_kwargs = {} + # This is a deprecated field that TG still returns for backwards compatibility + # Let's filter it out to speed up the de-json process + if user_ids := data.get("user_ids"): + api_kwargs = {"user_ids": user_ids} + + return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) @property def user_ids(self) -> Tuple[int, ...]: diff --git a/tests/test_message.py b/tests/test_message.py index 343737b81d7..7f04dffbeb1 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -50,6 +50,7 @@ PollOption, ProximityAlertTriggered, ReplyParameters, + SharedUser, Sticker, Story, SuccessfulPayment, @@ -89,6 +90,7 @@ def message(bot): date=TestMessageBase.date, chat=copy(TestMessageBase.chat), from_user=copy(TestMessageBase.from_user), + business_connection_id="123456789", ) message.set_bot(bot) message._unfreeze() @@ -218,7 +220,7 @@ def message(bot): }, {"web_app_data": WebAppData("some_data", "some_button_text")}, {"message_thread_id": 123}, - {"users_shared": UsersShared(1, [2, 3])}, + {"users_shared": UsersShared(1, users=[SharedUser(2, "user2"), SharedUser(3, "user3")])}, {"chat_shared": ChatShared(3, 4)}, { "giveaway": Giveaway( @@ -1392,7 +1394,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_text, Bot.send_message, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1400,6 +1402,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_text, message.get_bot()) @@ -1430,7 +1433,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_markdown, Bot.send_message, - ["chat_id", "parse_mode", "reply_to_message_id"], + ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1438,6 +1441,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_text, message.get_bot()) @@ -1472,7 +1476,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_markdown_v2, Bot.send_message, - ["chat_id", "parse_mode", "reply_to_message_id"], + ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1480,6 +1484,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_text, message.get_bot()) @@ -1519,7 +1524,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_html, Bot.send_message, - ["chat_id", "parse_mode", "reply_to_message_id"], + ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1527,6 +1532,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_text, message.get_bot()) @@ -1552,7 +1558,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_media_group, Bot.send_media_group, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1560,6 +1566,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_media_group", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_media_group, message.get_bot()) @@ -1590,7 +1597,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_photo, Bot.send_photo, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1598,6 +1605,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_photo", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_photo, message.get_bot()) @@ -1620,7 +1628,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_audio, Bot.send_audio, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1628,6 +1636,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_audio", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_audio, message.get_bot()) @@ -1650,7 +1659,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_document, Bot.send_document, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1658,6 +1667,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_document", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_document, message.get_bot()) @@ -1680,7 +1690,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_animation, Bot.send_animation, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1688,6 +1698,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_animation", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_animation, message.get_bot()) @@ -1710,7 +1721,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_sticker, Bot.send_sticker, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1718,6 +1729,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_sticker", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_sticker, message.get_bot()) @@ -1740,7 +1752,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_video, Bot.send_video, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1748,6 +1760,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_video", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_video, message.get_bot()) @@ -1770,7 +1783,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_video_note, Bot.send_video_note, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1778,6 +1791,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_video_note", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_video_note, message.get_bot()) @@ -1800,7 +1814,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_voice, Bot.send_voice, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1808,6 +1822,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_voice", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_voice, message.get_bot()) @@ -1830,7 +1845,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_location, Bot.send_location, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1838,6 +1853,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_location", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_location, message.get_bot()) @@ -1860,7 +1876,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_venue, Bot.send_venue, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1868,6 +1884,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_venue", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_venue, message.get_bot()) @@ -1890,7 +1907,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_contact, Bot.send_contact, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1898,6 +1915,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_contact", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_contact, message.get_bot()) @@ -1921,11 +1939,15 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_poll, Bot.send_poll, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( - message.reply_poll, message.get_bot(), "send_poll", skip_params=["reply_to_message_id"] + message.reply_poll, + message.get_bot(), + "send_poll", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_poll, message.get_bot()) @@ -1948,11 +1970,15 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_dice, Bot.send_dice, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( - message.reply_dice, message.get_bot(), "send_dice", skip_params=["reply_to_message_id"] + message.reply_dice, + message.get_bot(), + "send_dice", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_dice, message.get_bot()) @@ -1977,10 +2003,16 @@ async def make_assertion(*_, **kwargs): return id_ and action assert check_shortcut_signature( - Message.reply_chat_action, Bot.send_chat_action, ["chat_id", "reply_to_message_id"], [] + Message.reply_chat_action, + Bot.send_chat_action, + ["chat_id", "reply_to_message_id", "business_connection_id"], + [], ) assert await check_shortcut_call( - message.reply_chat_action, message.get_bot(), "send_chat_action" + message.reply_chat_action, + message.get_bot(), + "send_chat_action", + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_chat_action, message.get_bot()) @@ -2004,11 +2036,15 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_game, Bot.send_game, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( - message.reply_game, message.get_bot(), "send_game", skip_params=["reply_to_message_id"] + message.reply_game, + message.get_bot(), + "send_game", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_game, message.get_bot()) @@ -2040,7 +2076,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_invoice, Bot.send_invoice, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -2048,6 +2084,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_invoice", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_invoice, message.get_bot()) @@ -2165,7 +2202,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_copy, Bot.copy_message, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call(message.copy, message.get_bot(), "copy_message") diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 19b41fa517f..89892741bd4 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -169,6 +169,7 @@ def ignored_param_requirements(object_name: str) -> set[str]: BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = { "create_new_sticker_set": {"sticker_format"}, # removed by bot api 7.2 "StickerSet": {"is_animated", "is_video"}, # removed by bot api 7.2 + "UsersShared": {"user_ids", "users"}, # removed/added by bot api 7.2 } diff --git a/tests/test_shared.py b/tests/test_shared.py index f12f51abb81..a0564f2992c 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -52,9 +52,10 @@ def test_de_json(self, bot): json_dict = { "request_id": self.request_id, "users": [user.to_dict() for user in self.users], + "user_ids": self.user_ids, } users_shared = UsersShared.de_json(json_dict, bot) - assert users_shared.api_kwargs == {} + assert users_shared.api_kwargs == {"user_ids": self.user_ids} assert users_shared.request_id == self.request_id assert users_shared.users == self.users From 0fb2322badcefa0b747d404c49b6361440f4c96e Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 8 Apr 2024 21:59:55 +0200 Subject: [PATCH 30/30] Fix docs and add TestSharedUser --- telegram/_shared.py | 6 +-- tests/test_shared.py | 106 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/telegram/_shared.py b/telegram/_shared.py index ee34691743b..f7d0a394872 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -238,7 +238,7 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatShared"] class SharedUser(TelegramObject): """ This object contains information about a user that was shared with the bot using a - :class:`telegram.KeyboardButtonRequestUser` button. + :class:`telegram.KeyboardButtonRequestUsers` button. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`user_id` is equal. @@ -274,8 +274,8 @@ class SharedUser(TelegramObject): bot. username (:obj:`str`): Optional. Username of the user, if the username was requested by the bot. - photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Available sizes of the chat photo, if - the photo was requested by the bot. + photo (Tuple[:class:`telegram.PhotoSize`]): Available sizes of the chat photo, if + the photo was requested by the bot. This list is empty if the photo was not requsted. """ __slots__ = ("first_name", "last_name", "photo", "user_id", "username") diff --git a/tests/test_shared.py b/tests/test_shared.py index a0564f2992c..fcad7ec345a 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -19,7 +19,7 @@ import pytest -from telegram import ChatShared, SharedUser, UsersShared +from telegram import ChatShared, PhotoSize, SharedUser, UsersShared from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @@ -82,6 +82,7 @@ def test_equality(self): b = UsersShared(self.request_id, users=self.users) c = UsersShared(1, users=self.users) d = UsersShared(self.request_id, users=(SharedUser(1, "user1"), SharedUser(1, "user2"))) + e = PhotoSize("file_id", "1", 1, 1) assert a == b assert hash(a) == hash(b) @@ -93,6 +94,9 @@ def test_equality(self): assert a != d assert hash(a) != hash(d) + assert a != e + assert hash(a) != hash(e) + @pytest.fixture(scope="class") def chat_shared(): @@ -131,11 +135,109 @@ def test_de_json(self, bot): assert chat_shared.request_id == self.request_id assert chat_shared.chat_id == self.chat_id - def test_equality(self): + def test_equality(self, users_shared): a = ChatShared(self.request_id, self.chat_id) b = ChatShared(self.request_id, self.chat_id) c = ChatShared(1, self.chat_id) d = ChatShared(self.request_id, 1) + e = users_shared + + assert a == b + assert hash(a) == hash(b) + assert a is not 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) + + +@pytest.fixture(scope="class") +def shared_user(): + return SharedUser( + TestSharedUserBase.user_id, + TestSharedUserBase.first_name, + last_name=TestSharedUserBase.last_name, + username=TestSharedUserBase.username, + photo=TestSharedUserBase.photo, + ) + + +class TestSharedUserBase: + user_id = 101112 + first_name = "first" + last_name = "last" + username = "user" + photo = ( + PhotoSize(file_id="file_id", width=1, height=1, file_unique_id="1"), + PhotoSize(file_id="file_id", width=2, height=2, file_unique_id="2"), + ) + + +class TestSharedUserWithoutRequest(TestSharedUserBase): + def test_slot_behaviour(self, shared_user): + for attr in shared_user.__slots__: + assert getattr(shared_user, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(shared_user)) == len(set(mro_slots(shared_user))), "duplicate slot" + + def test_to_dict(self, shared_user): + shared_user_dict = shared_user.to_dict() + + assert isinstance(shared_user_dict, dict) + assert shared_user_dict["user_id"] == self.user_id + assert shared_user_dict["first_name"] == self.first_name + assert shared_user_dict["last_name"] == self.last_name + assert shared_user_dict["username"] == self.username + assert shared_user_dict["photo"] == [photo.to_dict() for photo in self.photo] + + def test_de_json_required(self, bot): + json_dict = { + "user_id": self.user_id, + "first_name": self.first_name, + } + shared_user = SharedUser.de_json(json_dict, bot) + assert shared_user.api_kwargs == {} + + assert shared_user.user_id == self.user_id + assert shared_user.first_name == self.first_name + assert shared_user.last_name is None + assert shared_user.username is None + assert shared_user.photo == () + + def test_de_json_all(self, bot): + json_dict = { + "user_id": self.user_id, + "first_name": self.first_name, + "last_name": self.last_name, + "username": self.username, + "photo": [photo.to_dict() for photo in self.photo], + } + shared_user = SharedUser.de_json(json_dict, bot) + assert shared_user.api_kwargs == {} + + assert shared_user.user_id == self.user_id + assert shared_user.first_name == self.first_name + assert shared_user.last_name == self.last_name + assert shared_user.username == self.username + assert shared_user.photo == self.photo + + assert SharedUser.de_json({}, bot) is None + + def test_equality(self, chat_shared): + a = SharedUser( + self.user_id, + self.first_name, + last_name=self.last_name, + username=self.username, + photo=self.photo, + ) + b = SharedUser(self.user_id, "other_firs_name") + c = SharedUser(self.user_id + 1, self.first_name) + d = chat_shared assert a == b assert hash(a) == hash(b) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy