From 5fa457974d6085f6dbd4f5e1fe62c82204450a19 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Fri, 12 Apr 2024 05:58:25 -0400 Subject: [PATCH] API 7.2 (#4180, #4181) Co-authored-by: Mahdyar Hasanpour Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Co-authored-by: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Co-authored-by: Aditya --- README.rst | 4 +- README_RAW.rst | 4 +- docs/source/inclusions/bot_methods.rst | 12 +- docs/source/telegram.at-tree.rst | 8 + docs/source/telegram.birthdate.rst | 7 + docs/source/telegram.businessconnection.rst | 6 + docs/source/telegram.businessintro.rst | 6 + docs/source/telegram.businesslocation.rst | 6 + .../telegram.businessmessagesdeleted.rst | 6 + docs/source/telegram.businessopeninghours.rst | 6 + .../telegram.businessopeninghoursinterval.rst | 6 + ...telegram.ext.businessconnectionhandler.rst | 6 + ...ram.ext.businessmessagesdeletedhandler.rst | 6 + docs/source/telegram.ext.handlers-tree.rst | 2 + docs/source/telegram.shareduser.rst | 7 + docs/substitutions/global.rst | 2 + pyproject.toml | 2 +- telegram/__init__.py | 19 +- telegram/_birthdate.py | 88 ++++ telegram/_bot.py | 220 ++++++++- telegram/_business.py | 445 ++++++++++++++++++ telegram/_chat.py | 113 ++++- telegram/_files/inputsticker.py | 22 +- telegram/_files/sticker.py | 41 +- telegram/_keyboardbuttonrequest.py | 60 ++- telegram/_message.py | 86 ++++ .../_passport/encryptedpassportelement.py | 16 +- telegram/_shared.py | 236 +++++++++- telegram/_update.py | 131 +++++- telegram/_user.py | 54 ++- telegram/constants.py | 35 +- telegram/ext/__init__.py | 4 + telegram/ext/_extbot.py | 89 +++- .../_handlers/businessconnectionhandler.py | 95 ++++ .../businessmessagesdeletedhandler.py | 95 ++++ telegram/ext/filters.py | 92 +++- tests/_files/test_inputsticker.py | 12 +- tests/_files/test_sticker.py | 129 +++-- tests/ext/test_businessconnectionhandler.py | 173 +++++++ .../test_businessmessagesdeletedhandler.py | 170 +++++++ tests/ext/test_callbackcontext.py | 6 +- tests/ext/test_filters.py | 43 ++ tests/request/test_requestparameter.py | 2 +- tests/test_birthdate.py | 83 ++++ tests/test_bot.py | 36 +- tests/test_business.py | 412 ++++++++++++++++ tests/test_chat.py | 33 ++ tests/test_constants.py | 1 + tests/test_message.py | 97 ++-- tests/test_official/exceptions.py | 6 +- tests/test_shared.py | 143 +++++- tests/test_update.py | 38 ++ tests/test_user.py | 5 + 53 files changed, 3264 insertions(+), 162 deletions(-) create mode 100644 docs/source/telegram.birthdate.rst create mode 100644 docs/source/telegram.businessconnection.rst create mode 100644 docs/source/telegram.businessintro.rst create mode 100644 docs/source/telegram.businesslocation.rst create mode 100644 docs/source/telegram.businessmessagesdeleted.rst create mode 100644 docs/source/telegram.businessopeninghours.rst create mode 100644 docs/source/telegram.businessopeninghoursinterval.rst create mode 100644 docs/source/telegram.ext.businessconnectionhandler.rst create mode 100644 docs/source/telegram.ext.businessmessagesdeletedhandler.rst create mode 100644 docs/source/telegram.shareduser.rst create mode 100644 telegram/_birthdate.py create mode 100644 telegram/_business.py create mode 100644 telegram/ext/_handlers/businessconnectionhandler.py create mode 100644 telegram/ext/_handlers/businessmessagesdeletedhandler.py create mode 100644 tests/ext/test_businessconnectionhandler.py create mode 100644 tests/ext/test_businessmessagesdeletedhandler.py create mode 100644 tests/test_birthdate.py create mode 100644 tests/test_business.py diff --git a/README.rst b/README.rst index 8366a1ff6..1d6b20aaf 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 fae3d516e..df1312e48 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 1f05f11ff..9dcfa1982 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 ffa7107b8..3d7829258 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 000000000..083de5ebf --- /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 000000000..3ef31c3b2 --- /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 000000000..4870258e5 --- /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 000000000..1a1b8893b --- /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 000000000..ba0e88e3c --- /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 000000000..cab989c84 --- /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 000000000..241379dbc --- /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 000000000..0b0509dff --- /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 000000000..840f19325 --- /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 e5df80b2c..6749cacb9 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 000000000..52dd3885b --- /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 050a6d52b..36038e71e 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 b941a2444..34c2a7637 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 162ba3d0e..304425c4d 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -22,6 +22,7 @@ __author__ = "devs@python-telegram-bot.org" __all__ = ( "Animation", "Audio", + "Birthdate", "Bot", "BotCommand", "BotCommandScope", @@ -35,6 +36,12 @@ __all__ = ( "BotDescription", "BotName", "BotShortDescription", + "BusinessConnection", + "BusinessIntro", + "BusinessLocation", + "BusinessMessagesDeleted", + "BusinessOpeningHours", + "BusinessOpeningHoursInterval", "CallbackGame", "CallbackQuery", "Chat", @@ -184,6 +191,7 @@ __all__ = ( "SecureData", "SecureValue", "SentWebAppMessage", + "SharedUser", "ShippingAddress", "ShippingOption", "ShippingQuery", @@ -224,6 +232,7 @@ __all__ = ( 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 ._botcommandscope import ( ) 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 ._reply import ExternalReplyInfo, ReplyParameters, TextQuote 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 000000000..d47a037a3 --- /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 a792e0f94..f9682f58b 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._botcommand import BotCommand 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): "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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): "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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. """ 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. .. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. .. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. "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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. """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 000000000..8d036a2cd --- /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 915614bb6..a6b7928e1 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -23,6 +23,7 @@ from html import escape 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 @@ if TYPE_CHECKING: 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): ) 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): pool_timeout=pool_timeout, api_kwargs=api_kwargs, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_document( @@ -1814,6 +1903,7 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 @@ class Chat(TelegramObject): 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 bfcd89300..192bef214 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 @@ class InputSticker(TelegramObject): 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 cb7b5deac..e939b1597 100644 --- a/telegram/_files/sticker.py +++ b/telegram/_files/sticker.py @@ -27,6 +27,8 @@ from telegram._telegramobject import TelegramObject 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 @@ class StickerSet(TelegramObject): 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 @@ class StickerSet(TelegramObject): 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 78bb2e505..bd12d27ad 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 @@ class KeyboardButtonRequestUsers(TelegramObject): 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 @@ class KeyboardButtonRequestUsers(TelegramObject): 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 @@ class KeyboardButtonRequestChat(TelegramObject): 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 @@ class KeyboardButtonRequestChat(TelegramObject): ) 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 18ee981c2..e2a71b715 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 @@ class Message(MaybeInaccessibleMessage): 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 14d8b63dc..e6a22ee2e 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 89cb0b5d6..f7d0a3948 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 a597f7792..d7dc727b8 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 @@ class Update(TelegramObject): 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 @@ class Update(TelegramObject): 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 @@ class Update(TelegramObject): * :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 @@ class Update(TelegramObject): 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 @@ class Update(TelegramObject): * :attr:`chat_boost` * :attr:`removed_chat_boost` * :attr:`message_reaction_count` + * :attr:`deleted_business_messages` is present. @@ -482,7 +571,12 @@ class Update(TelegramObject): 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 @@ class Update(TelegramObject): 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 @@ class Update(TelegramObject): 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 @@ class Update(TelegramObject): :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 @@ class Update(TelegramObject): 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 @@ class Update(TelegramObject): 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 eb4227e18..8ceb4d0d4 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + business_connection_id=business_connection_id, ) async def send_audio( @@ -627,6 +647,7 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 @@ class User(TelegramObject): 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 959e99ac4..c43ed6d13 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 d1101bcf2..82dbd1c19 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -27,6 +27,8 @@ __all__ = ( "BasePersistence", "BaseRateLimiter", "BaseUpdateProcessor", + "BusinessConnectionHandler", + "BusinessMessagesDeletedHandler", "CallbackContext", "CallbackDataCache", "CallbackQueryHandler", @@ -75,6 +77,8 @@ from ._defaults import Defaults 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 30ae00299..7b5649ebe 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -48,6 +48,7 @@ from telegram import ( BotDescription, BotName, BotShortDescription, + BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -571,6 +572,7 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): audio=audio, duration=duration, performer=performer, + business_connection_id=business_connection_id, title=title, caption=caption, disable_notification=disable_notification, @@ -2449,6 +2456,7 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): ) -> 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 000000000..21336dcef --- /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 000000000..14fed0b5a --- /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 34bede100..961ba9bf2 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -54,6 +54,7 @@ __all__ = ( "HAS_PROTECTED_CONTENT", "INVOICE", "IS_AUTOMATIC_FORWARD", + "IS_FROM_OFFLINE", "IS_TOPIC_MESSAGE", "LOCATION", "PASSPORT_DATA", @@ -272,20 +273,28 @@ class BaseFilter: 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 @@ IS_TOPIC_MESSAGE = _IsTopicMessage(name="filters.IS_TOPIC_MESSAGE") """ +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 UpdateType: __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 @@ class UpdateType: 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 @@ class _ViaBot(MessageFilter): 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 c974853f2..680a01670 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 @@ class TestInputStickerNoRequest(TestInputStickerBase): 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 @@ class TestInputStickerNoRequest(TestInputStickerBase): 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 3cc8c584d..c40846811 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -39,6 +39,7 @@ from telegram import ( 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 @@ class TestStickerWithRequest(TestStickerBase): 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 @@ class TestStickerWithRequest(TestStickerBase): } 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 @@ class TestStickerWithRequest(TestStickerBase): @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 @@ class TestStickerSetWithoutRequest(TestStickerSetBase): 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 @@ class TestStickerSetWithoutRequest(TestStickerSetBase): 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 @@ class TestStickerSetWithoutRequest(TestStickerSetBase): 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 @@ class TestStickerSetWithoutRequest(TestStickerSetBase): 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 @@ class TestStickerSetWithoutRequest(TestStickerSetBase): ) 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 @@ class TestStickerSetWithoutRequest(TestStickerSetBase): 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 @@ class TestStickerSetWithoutRequest(TestStickerSetBase): 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 @@ class TestStickerSetWithoutRequest(TestStickerSetBase): 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 @@ class TestStickerSetWithoutRequest(TestStickerSetBase): 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 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: sticker=sticker_file, emoji_list=["😄"], mask_position=MaskPosition(MaskPosition.EYES, -1, 1, 2), + format=StickerFormat.STATIC, ), ), ) @@ -915,7 +937,9 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: 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 @@ class TestStickerSetWithRequest: 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 @@ class TestMaskPositionWithoutRequest(TestMaskPositionBase): 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 @@ class TestMaskPositionWithRequest(TestMaskPositionBase): 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 000000000..c8d741332 --- /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 000000000..a15a0a0c2 --- /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 ed4f014b2..0a099f64f 100644 --- a/tests/ext/test_callbackcontext.py +++ b/tests/ext/test_callbackcontext.py @@ -32,6 +32,7 @@ from telegram import ( 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 @@ class TestCallbackContext: 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 bcd198091..694ea009a 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -2035,6 +2035,11 @@ class TestFilters: 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 @@ class TestFilters: 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 @@ class TestFilters: 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 @@ class TestFilters: 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 @@ class TestFilters: 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 59a3e3f91..d7ad2088a 100644 --- a/tests/request/test_requestparameter.py +++ b/tests/request/test_requestparameter.py @@ -163,7 +163,7 @@ class TestRequestParameterWithoutRequest: 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 000000000..4c028661a --- /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 853d9c305..7021867da 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -39,6 +39,7 @@ from telegram import ( BotDescription, BotName, BotShortDescription, + BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -2087,6 +2088,37 @@ class TestBotWithoutRequest: 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 @@ class TestBotWithRequest: 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 000000000..da6838d6d --- /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 7db747146..11ef38dda 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -21,7 +21,12 @@ import datetime 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 @@ class TestChatWithoutRequest(TestChatBase): "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 @@ class TestChatWithoutRequest(TestChatBase): "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 @@ class TestChatWithoutRequest(TestChatBase): 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 @@ class TestChatWithoutRequest(TestChatBase): 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 @@ class TestChatWithoutRequest(TestChatBase): 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 @@ class TestChatWithoutRequest(TestChatBase): ) 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 d99dbc8bd..98768b806 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -176,6 +176,7 @@ class TestConstantsWithoutRequest: # 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 d0db5aeb2..7f04dffbe 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -50,6 +50,7 @@ from telegram import ( 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 7807a0278..89892741b 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 3c76eb329..fcad7ec34 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 @@ class TestUsersSharedWithoutRequest(TestUsersSharedBase): 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 @@ class TestUsersSharedWithoutRequest(TestUsersSharedBase): 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 @@ class TestChatSharedWithoutRequest(TestChatSharedBase): 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 e46608f8a..b314c98e8 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -23,6 +23,8 @@ from datetime import datetime import pytest from telegram import ( + BusinessConnection, + BusinessMessagesDeleted, CallbackQuery, Chat, ChatBoost, @@ -119,6 +121,28 @@ message_reaction_count = MessageReactionCountUpdated( 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 @@ params = [ {"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 @@ all_types = ( "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 @@ class TestUpdateWithoutRequest(TestUpdateBase): 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 @@ class TestUpdateWithoutRequest(TestUpdateBase): 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 @@ class TestUpdateWithoutRequest(TestUpdateBase): 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 @@ class TestUpdateWithoutRequest(TestUpdateBase): 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 @@ class TestUpdateWithoutRequest(TestUpdateBase): 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 9edc0db6b..86faa73cd 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 @@ class TestUserWithoutRequest(TestUserBase): 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 @@ class TestUserWithoutRequest(TestUserBase): 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)