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/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 1f05f11ff11..9dcfa1982e2 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` @@ -237,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/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index ffa7107b89e..3d78292588a 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 @@ -18,6 +19,12 @@ Available Types telegram.botdescription telegram.botname telegram.botshortdescription + telegram.businessconnection + telegram.businessintro + telegram.businesslocation + telegram.businessopeninghours + telegram.businessopeninghoursinterval + telegram.businessmessagesdeleted telegram.callbackquery telegram.chat telegram.chatadministratorrights @@ -107,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.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/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.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.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/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/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.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/docs/source/telegram.ext.handlers-tree.rst b/docs/source/telegram.ext.handlers-tree.rst index e5df80b2cc6..6749cacb9dd 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.businessmessagesdeletedhandler telegram.ext.callbackqueryhandler telegram.ext.chatboosthandler telegram.ext.chatjoinrequesthandler 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/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/__init__.py b/telegram/__init__.py index 162ba3d0edb..304425c4d61 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -22,6 +22,7 @@ __all__ = ( "Animation", "Audio", + "Birthdate", "Bot", "BotCommand", "BotCommandScope", @@ -35,6 +36,12 @@ "BotDescription", "BotName", "BotShortDescription", + "BusinessConnection", + "BusinessIntro", + "BusinessLocation", + "BusinessMessagesDeleted", + "BusinessOpeningHours", + "BusinessOpeningHoursInterval", "CallbackGame", "CallbackQuery", "Chat", @@ -184,6 +191,7 @@ "SecureData", "SecureValue", "SentWebAppMessage", + "SharedUser", "ShippingAddress", "ShippingOption", "ShippingQuery", @@ -224,6 +232,7 @@ from . import _version, constants, error, helpers, request, warnings +from ._birthdate import Birthdate from ._bot import Bot from ._botcommand import BotCommand from ._botcommandscope import ( @@ -238,6 +247,14 @@ ) from ._botdescription import BotDescription, BotShortDescription from ._botname import BotName +from ._business import ( + BusinessConnection, + BusinessIntro, + BusinessLocation, + BusinessMessagesDeleted, + BusinessOpeningHours, + BusinessOpeningHoursInterval, +) from ._callbackquery import CallbackQuery from ._chat import Chat from ._chatadministratorrights import ChatAdministratorRights @@ -393,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/_birthdate.py b/telegram/_birthdate.py new file mode 100644 index 00000000000..d47a037a31e --- /dev/null +++ b/telegram/_birthdate.py @@ -0,0 +1,88 @@ +#!/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 datetime import datetime +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() + + 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 a792e0f9490..f9682f58b86 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 @@ -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 @@ -667,6 +668,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 +725,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 +916,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 +962,9 @@ async def send_message( 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| @@ -1009,6 +1018,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 +1269,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 +1330,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 +1390,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 +1409,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 +1479,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| @@ -1523,6 +1542,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( @@ -1539,6 +1559,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 +1628,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 +1687,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 +1700,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, @@ -1693,8 +1719,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. @@ -1723,6 +1749,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 +1800,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 +1821,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 +1899,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 +1964,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 +1979,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 +2042,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 +2101,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 +2121,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 +2193,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 +2257,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 +2273,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 +2338,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 +2398,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 +2411,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 +2459,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 +2554,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 +2583,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 +2635,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 +2706,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 +2866,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 +2916,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 +2998,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 +3013,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 +3053,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 +3126,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 +3138,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 +3168,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 +3216,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 +3224,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 +3246,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 +3261,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", @@ -6147,9 +6227,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. @@ -6234,7 +6312,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, *, @@ -6287,6 +6365,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 @@ -6306,6 +6387,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, @@ -6399,6 +6488,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, @@ -6412,9 +6502,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` @@ -6447,6 +6549,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( @@ -6728,6 +6831,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 +6907,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 +6969,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 +7026,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 +7070,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 +7119,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( @@ -8665,6 +8778,95 @@ 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, + ) + + 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} @@ -8911,3 +9113,7 @@ 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`""" + replaceStickerInSet = replace_sticker_in_set + """Alias for :meth:`replace_sticker_in_set`""" diff --git a/telegram/_business.py b/telegram/_business.py new file mode 100644 index 00000000000..8d036a2cdbc --- /dev/null +++ b/telegram/_business.py @@ -0,0 +1,445 @@ +#!/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, 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 +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: 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, + 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 (Tuple[: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: str = business_connection_id + self.chat: Chat = chat + self.message_ids: Tuple[int, ...] = 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) + + +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 915614bb6c8..a6b7928e157 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 @@ -44,6 +45,9 @@ Animation, Audio, Bot, + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, ChatInviteLink, ChatMember, Contact, @@ -169,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 @@ -229,6 +248,14 @@ 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 + 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: id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits @@ -312,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 @@ -372,6 +414,14 @@ 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 + 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 .. _accent colors: https://core.telegram.org/bots/api#accent-colors @@ -383,6 +433,10 @@ class Chat(TelegramObject): "available_reactions", "background_custom_emoji_id", "bio", + "birthdate", + "business_intro", + "business_location", + "business_opening_hours", "can_set_sticker_set", "custom_emoji_sticker_set_name", "description", @@ -405,6 +459,7 @@ class Chat(TelegramObject): "location", "message_auto_delete_time", "permissions", + "personal_chat", "photo", "pinned_message", "profile_accent_color_id", @@ -470,6 +525,11 @@ 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, + 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, ): @@ -519,6 +579,11 @@ 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.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,) @@ -581,12 +646,24 @@ 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) 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) + 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 @@ -1444,6 +1521,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 +1561,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 +1637,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 +1678,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 +1712,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 +1730,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 +1771,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 +1785,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 +1825,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 +1843,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 +1887,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 +1903,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 +1945,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 +1956,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 +1991,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 +2002,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 +2037,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 +2147,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 +2189,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 +2208,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 +2253,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 +2265,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 +2301,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 +2319,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 +2363,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 +2383,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 +2429,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 +2443,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 +2483,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 +2498,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 +2539,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 +2561,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 +2607,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/_files/inputsticker.py b/telegram/_files/inputsticker.py index bfcd89300a2..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,6 +56,13 @@ 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`): 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. @@ -67,15 +78,23 @@ 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`): 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", "keywords", "mask_position", "sticker") + __slots__ = ("emoji_list", "format", "keywords", "mask_position", "sticker") 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, *, @@ -91,6 +110,7 @@ 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) diff --git a/telegram/_files/sticker.py b/telegram/_files/sticker.py index cb7b5deac0b..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 @@ -227,6 +229,11 @@ class StickerSet(TelegramObject): .. versionchanged:: 20.0 The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` instead. + + .. versionchanged:: NEXT.VERSION + 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| @@ -234,9 +241,16 @@ class StickerSet(TelegramObject): 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. + .. 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 @@ -256,9 +270,16 @@ class StickerSet(TelegramObject): 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. + .. 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 @@ -289,10 +310,10 @@ def __init__( self, name: str, title: str, - is_animated: bool, stickers: Sequence[Sticker], - is_video: bool, sticker_type: str, + is_animated: Optional[bool] = None, + is_video: Optional[bool] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, @@ -300,13 +321,19 @@ 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 - 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/_keyboardbuttonrequest.py b/telegram/_keyboardbuttonrequest.py index 78bb2e50545..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,6 +57,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 Attributes: request_id (:obj:`int`): Identifier of the request. @@ -71,11 +82,25 @@ 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__ = ( "max_quantity", "request_id", + "request_name", + "request_photo", + "request_username", "user_is_bot", "user_is_premium", ) @@ -86,6 +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, *, api_kwargs: Optional[JSONDict] = None, ): @@ -97,6 +125,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 +169,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 +185,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 +199,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__ = ( @@ -169,6 +218,9 @@ class KeyboardButtonRequestChat(TelegramObject): "chat_is_created", "chat_is_forum", "request_id", + "request_photo", + "request_title", + "request_username", "user_administrator_rights", ) @@ -182,6 +234,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 +254,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/_message.py b/telegram/_message.py index 18ee981c2cb..e2a71b7155f 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 @@ -301,6 +302,11 @@ 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. + + .. 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, @@ -534,6 +540,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:`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 Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -568,6 +586,11 @@ 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. + + .. 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, @@ -817,6 +840,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:`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 + .. |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. @@ -836,6 +872,7 @@ class Message(MaybeInaccessibleMessage): "audio", "author_signature", "boost_added", + "business_connection_id", "caption", "caption_entities", "channel_chat_created", @@ -866,6 +903,7 @@ class Message(MaybeInaccessibleMessage): "has_protected_content", "invoice", "is_automatic_forward", + "is_from_offline", "is_topic_message", "left_chat_member", "link_preview_options", @@ -888,6 +926,7 @@ class Message(MaybeInaccessibleMessage): "reply_to_message", "reply_to_story", "sender_boost_count", + "sender_business_bot", "sender_chat", "sticker", "story", @@ -987,6 +1026,9 @@ 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, + is_from_offline: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1082,6 +1124,9 @@ 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.is_from_offline: Optional[bool] = is_from_offline self._effective_attachment = DEFAULT_NONE @@ -1224,6 +1269,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 @@ -1575,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, ) @@ -1620,6 +1667,7 @@ async def reply_text( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_markdown( @@ -1650,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, ) @@ -1700,6 +1749,7 @@ async def reply_markdown( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_markdown_v2( @@ -1730,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, ) @@ -1776,6 +1827,7 @@ async def reply_markdown_v2( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_html( @@ -1806,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, ) @@ -1852,6 +1905,7 @@ async def reply_html( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_media_group( @@ -1882,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, ) @@ -1927,6 +1982,7 @@ async def reply_media_group( caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + business_connection_id=self.business_connection_id, ) async def reply_photo( @@ -1958,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, ) @@ -2004,6 +2061,7 @@ async def reply_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, has_spoiler=has_spoiler, + business_connection_id=self.business_connection_id, ) async def reply_audio( @@ -2038,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, ) @@ -2087,6 +2146,7 @@ async def reply_audio( pool_timeout=pool_timeout, api_kwargs=api_kwargs, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_document( @@ -2119,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, ) @@ -2166,6 +2227,7 @@ async def reply_document( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_animation( @@ -2201,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, ) @@ -2251,6 +2314,7 @@ async def reply_animation( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_sticker( @@ -2278,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, ) @@ -2320,6 +2385,7 @@ async def reply_sticker( protect_content=protect_content, message_thread_id=message_thread_id, emoji=emoji, + business_connection_id=self.business_connection_id, ) async def reply_video( @@ -2356,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, ) @@ -2407,6 +2474,7 @@ async def reply_video( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_video_note( @@ -2437,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, ) @@ -2482,6 +2551,7 @@ async def reply_video_note( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_voice( @@ -2513,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, ) @@ -2559,6 +2630,7 @@ async def reply_voice( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, ) async def reply_location( @@ -2591,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, ) @@ -2638,6 +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=self.business_connection_id, ) async def reply_venue( @@ -2672,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, ) @@ -2721,6 +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=self.business_connection_id, ) async def reply_contact( @@ -2751,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, ) @@ -2796,6 +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=self.business_connection_id, ) async def reply_poll( @@ -2833,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, ) @@ -2885,6 +2964,7 @@ async def reply_poll( explanation_entities=explanation_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, ) async def reply_dice( @@ -2911,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, ) @@ -2952,6 +3033,7 @@ 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=self.business_connection_id, ) async def reply_chat_action( @@ -2970,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, ) @@ -2994,6 +3077,7 @@ async def reply_chat_action( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_game( @@ -3020,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, ) @@ -3063,6 +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=self.business_connection_id, ) async def reply_invoice( 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 diff --git a/telegram/_shared.py b/telegram/_shared.py index 89cb0b5d6a2..f7d0a394872 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -17,10 +17,21 @@ # 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._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 ( + 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): @@ -29,48 +40,118 @@ 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. - user_ids (Sequence[: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. + + .. 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. + + .. deprecated:: NEXT.VERSION + 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. - 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 (Tuple[:class:`telegram.SharedUser`]): Information about users shared with the + bot. + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("request_id", "user_ids") + __slots__ = ("request_id", "users") def __init__( self, request_id: int, - user_ids: Sequence[int], + 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 - self.user_ids: Tuple[int, ...] = tuple(user_ids) - self._id_attrs = (self.request_id, self.user_ids) + if users is None: + raise TypeError("`users` is a required argument since Bot API 7.2") + + self.users: Tuple[SharedUser, ...] = parse_sequence_arg(users) + + 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() + @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) + + 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, ...]: + """ + 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 + 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", + 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 +169,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 +187,127 @@ 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", "photo", "request_id", "title", "username") 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.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. + + .. 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`]): 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") + + def __init__( + self, + user_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] = last_name + 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) diff --git a/telegram/_update.py b/telegram/_update.py index a597f7792f8..d7dc727b86e 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,9 +467,14 @@ def effective_user(self) -> Optional["User"]: * :attr:`chat_boost` * :attr:`removed_chat_boost` * :attr:`message_reaction_count` + * :attr:`deleted_business_messages` 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`. @@ -443,6 +522,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 +551,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 +571,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 +600,12 @@ 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. + + .. 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`. @@ -554,6 +652,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 @@ -566,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 @@ -608,6 +719,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 +760,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/_user.py b/telegram/_user.py index eb4227e18ce..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,) @@ -393,6 +407,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 +450,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 +529,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 +573,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 +585,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 +629,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 +647,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 +694,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 +731,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 +748,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 +791,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 +802,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 +840,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 +856,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 +901,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 +912,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 +950,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 +1063,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 +1108,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 +1127,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 +1175,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 +1187,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 +1226,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 +1246,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 +1295,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 +1313,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 +1360,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 +1374,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 +1417,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 +1432,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 +1476,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 +1498,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 +1547,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..c43ed6d130d 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__`. @@ -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" @@ -2312,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 @@ -2504,6 +2517,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/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/_extbot.py b/telegram/ext/_extbot.py index 30ae0029999..7b5649ebea3 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -48,6 +48,7 @@ BotDescription, BotName, BotShortDescription, + BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -571,6 +572,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 +603,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) @@ -1182,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, *, @@ -2355,6 +2358,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 +2392,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 +2413,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 +2430,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 +2456,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 +2467,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 +2489,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 +2519,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 +2532,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 +2546,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 +2575,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 +2599,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 +2620,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 +2637,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 +2739,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 +2770,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 +2785,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 +2814,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 +2831,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 +2850,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 +2878,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 +2905,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 +2933,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 +2962,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 +2983,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 +3000,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 +3029,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 +3056,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 +3085,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 +3113,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 +3137,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 +3168,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 +3184,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 +3216,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( @@ -3466,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, @@ -3479,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, @@ -4002,6 +4041,52 @@ 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), + ) + + 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 @@ -4121,3 +4206,5 @@ 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 + replaceStickerInSet = replace_sticker_in_set diff --git a/telegram/ext/_handlers/businessconnectionhandler.py b/telegram/ext/_handlers/businessconnectionhandler.py new file mode 100644 index 00000000000..21336dceff8 --- /dev/null +++ b/telegram/ext/_handlers/businessconnectionhandler.py @@ -0,0 +1,95 @@ +#!/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 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). + + 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 + 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 new file mode 100644 index 00000000000..14fed0b5a88 --- /dev/null +++ b/telegram/ext/_handlers/businessmessagesdeletedhandler.py @@ -0,0 +1,95 @@ +#!/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 from the specified chat ID(s). + + username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only + 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 + :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 + 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 34bede100e5..961ba9bf228 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", @@ -272,20 +273,28 @@ 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. 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. """ return bool( # 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 ) @@ -1554,6 +1563,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. @@ -2486,13 +2509,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): @@ -2530,7 +2561,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): @@ -2675,6 +2747,8 @@ class ViaBot(_ChatUserBaseFilter): Examples: ``MessageHandler(filters.ViaBot(1234), callback_method)`` + .. seealso:: :attr:`~telegram.ext.filters.VIA_BOT` + Args: bot_id(:obj:`int` | Collection[:obj:`int`], optional): Which bot ID(s) to allow through. @@ -2756,7 +2830,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:: :class:`~telegram.ext.filters.ViaBot`""" class _Video(MessageFilter): diff --git a/tests/_files/test_inputsticker.py b/tests/_files/test_inputsticker.py index c974853f268..680a0167099 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,9 +42,10 @@ class TestInputStickerBase: emoji_list = ("👍", "👎") mask_position = MaskPosition("forehead", 0.5, 0.5, 0.5) keywords = ("thumbsup", "thumbsdown") + format = "static" -class TestInputStickerNoRequest(TestInputStickerBase): +class TestInputStickerWithoutRequest(TestInputStickerBase): def test_slot_behaviour(self, input_sticker): inst = input_sticker for attr in inst.__slots__: @@ -56,11 +58,12 @@ 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) 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 == () @@ -72,9 +75,10 @@ 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=["👍"]) + 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/_files/test_sticker.py b/tests/_files/test_sticker.py index 3cc8c584dc4..c408468118a 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, @@ -471,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. @@ -489,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 @@ -528,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: @@ -543,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: @@ -578,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 @@ -588,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" @@ -598,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, @@ -609,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 @@ -622,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 @@ -632,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) @@ -689,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 @@ -719,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 @@ -759,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 @@ -782,7 +768,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 @@ -798,9 +784,29 @@ 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", + ) + + 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") -@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 @@ -822,8 +828,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"): @@ -831,8 +840,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"): @@ -840,8 +854,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 @@ -855,8 +872,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 @@ -875,8 +891,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, "") @@ -895,7 +914,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, @@ -904,6 +925,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, ), ), ) @@ -915,7 +937,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, ), ) @@ -925,7 +949,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 @@ -948,7 +972,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, format="static", thumbnail=sticker_set_thumb_file ) async def test_bot_methods_3_tgs( @@ -958,8 +982,13 @@ 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", + thumbnail=animated_sticker_file, + ), + bot.set_sticker_set_thumbnail(animated_test, chat_id, "animated", thumbnail=file_id), ) assert all(await tasks) @@ -1042,6 +1071,19 @@ 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=["😄"], + format=StickerFormat.STATIC, + ), + ) + @pytest.fixture(scope="module") def mask_position(): @@ -1112,7 +1154,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}" @@ -1132,9 +1173,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/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_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)) diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index bcd1980914e..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 @@ -2343,6 +2348,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 +2361,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 +2374,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 +2387,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" 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_birthdate.py b/tests/test_birthdate.py new file mode 100644 index 00000000000..4c028661ac8 --- /dev/null +++ b/tests/test_birthdate.py @@ -0,0 +1,83 @@ +#!/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 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_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) + 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) + + 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 853d9c305a5..7021867da64 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -39,6 +39,7 @@ BotDescription, BotName, BotShortDescription, + BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -2087,6 +2088,37 @@ 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. + 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 + + 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: """ @@ -3373,8 +3405,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. diff --git a/tests/test_business.py b/tests/test_business.py new file mode 100644 index 00000000000..da6838d6d47 --- /dev/null +++ b/tests/test_business.py @@ -0,0 +1,412 @@ +#!/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, + BusinessIntro, + BusinessLocation, + BusinessMessagesDeleted, + BusinessOpeningHours, + BusinessOpeningHoursInterval, + Chat, + Location, + Sticker, + 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") + 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") +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, + ) + + +@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 + 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) + + +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 7db747146a8..11ef38dda15 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -21,7 +21,12 @@ import pytest from telegram import ( + Birthdate, Bot, + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, + BusinessOpeningHoursInterval, Chat, ChatLocation, ChatPermissions, @@ -74,6 +79,11 @@ 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, ) chat.set_bot(bot) chat._unfreeze() @@ -113,12 +123,20 @@ 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 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) + personal_chat = Chat(3, "private", "private") class TestChatWithoutRequest(TestChatBase): @@ -139,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, @@ -162,6 +183,8 @@ 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(), + "personal_chat": self.personal_chat.to_dict(), } chat = Chat.de_json(json_dict, bot) @@ -174,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 @@ -202,6 +228,8 @@ 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 + assert chat.personal_chat == self.personal_chat def test_de_json_localization(self, bot, raw_bot, tz_bot): json_dict = { @@ -234,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 @@ -267,6 +298,8 @@ 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( diff --git a/tests/test_constants.py b/tests/test_constants.py index d99dbc8bdbd..98768b806b4 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", } @pytest.mark.parametrize( diff --git a/tests/test_message.py b/tests/test_message.py index d0db5aeb272..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( @@ -263,6 +265,9 @@ 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"}, ], ids=[ "reply", @@ -328,6 +333,9 @@ def message(bot): "reply_to_story", "boost_added", "sender_boost_count", + "sender_business_bot", + "business_connection_id", + "is_from_offline", ], ) def message_params(bot, request): @@ -1386,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( @@ -1394,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()) @@ -1424,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( @@ -1432,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()) @@ -1466,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( @@ -1474,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()) @@ -1513,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( @@ -1521,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()) @@ -1546,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( @@ -1554,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()) @@ -1584,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( @@ -1592,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()) @@ -1614,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( @@ -1622,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()) @@ -1644,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( @@ -1652,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()) @@ -1674,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( @@ -1682,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()) @@ -1704,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( @@ -1712,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()) @@ -1734,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( @@ -1742,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()) @@ -1764,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( @@ -1772,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()) @@ -1794,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( @@ -1802,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()) @@ -1824,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( @@ -1832,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()) @@ -1854,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( @@ -1862,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()) @@ -1884,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( @@ -1892,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()) @@ -1915,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()) @@ -1942,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()) @@ -1971,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()) @@ -1998,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()) @@ -2034,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( @@ -2042,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()) @@ -2159,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 7807a02784a..89892741bd4 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -166,7 +166,11 @@ 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]] = {} +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 +} def backwards_compat_kwargs(object_name: str) -> set[str]: diff --git a/tests/test_shared.py b/tests/test_shared.py index 3c76eb329f5..fcad7ec345a 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, PhotoSize, 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,43 @@ 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, + "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 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"))) + e = PhotoSize("file_id", "1", 1, 1) assert a == b assert hash(a) == hash(b) @@ -74,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(): @@ -112,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) diff --git a/tests/test_update.py b/tests/test_update.py index e46608f8a46..b314c98e819 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,28 @@ 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), +) + +business_message = Message( + 1, + datetime.utcnow(), + Chat(1, ""), + User(1, "", False), +) + params = [ {"message": message}, @@ -150,6 +174,10 @@ {"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}, + {"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")}, ] @@ -173,6 +201,10 @@ "removed_chat_boost", "message_reaction", "message_reaction_count", + "business_connection", + "deleted_business_messages", + "business_message", + "edited_business_message", ) ids = (*all_types, "callback_query_without_message") @@ -257,6 +289,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 +305,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 +331,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 +364,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 +401,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: 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) 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