diff --git a/.github/workflows/dependabot-prs.yml b/.github/workflows/dependabot-prs.yml index a7634f028d6..5a971fbbdce 100644 --- a/.github/workflows/dependabot-prs.yml +++ b/.github/workflows/dependabot-prs.yml @@ -16,7 +16,7 @@ jobs: - name: Fetch Dependabot metadata id: dependabot-metadata - uses: dependabot/fetch-metadata@v1.6.0 + uses: dependabot/fetch-metadata@v2.0.0 - uses: actions/checkout@v4 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5acb00b026..55166a45339 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.2.1' + rev: 'v0.3.5' hooks: - id: ruff name: ruff @@ -17,7 +17,7 @@ repos: - cachetools~=5.3.3 - aiolimiter~=1.1.0 - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.1.1 + rev: 24.3.0 hooks: - id: black args: @@ -28,7 +28,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/PyCQA/pylint - rev: v3.0.3 + rev: v3.1.0 hooks: - id: pylint files: ^(?!(tests|docs)).*\.py$ @@ -40,7 +40,7 @@ repos: - aiolimiter~=1.1.0 - . # this basically does `pip install -e .` - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy name: mypy-ptb @@ -67,7 +67,7 @@ repos: - cachetools~=5.3.3 - . # this basically does `pip install -e .` - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.2 hooks: - id: pyupgrade args: diff --git a/CHANGES.rst b/CHANGES.rst index d100fa3f5ba..1a7f3c0466c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,48 @@ Changelog ========= +Version 21.1 +============== + +*Released 2024-04-12* + +This is the technical changelog for version 21.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +Major Changes +------------- + +- API 7.2 (:pr:`4180` closes :issue:`4179` and :issue:`4181`, :issue:`4181`) +- Make ``ChatAdministratorRights/ChatMemberAdministrator.can_*_stories`` Required (API 7.1) (:pr:`4192`) + +Minor Changes +------------- + +- Refactor Debug logging in ``Bot`` to Improve Type Hinting (:pr:`4151` closes :issue:`4010`) + +New Features +------------ + +- Make ``Message.reply_*`` Reply in the Same Topic by Default (:pr:`4170` by `@aelkheir `__ closes :issue:`4139`) +- Accept Socket Objects for Webhooks (:pr:`4161` closes :issue:`4078`) +- Add ``Update.effective_sender`` (:pr:`4168` by `@aelkheir `__ closes :issue:`4085`) + +Documentation Improvements +-------------------------- + +- Documentation Improvements (:pr:`4171`, :pr:`4158` by `@teslaedison `__) + +Internal Changes +---------------- + +- Temporarily Mark Tests with ``get_sticker_set`` as XFAIL due to API 7.2 Update (:pr:`4190`) + +Dependency Updates +------------------ + +- ``pre-commit`` autoupdate (:pr:`4184`) +- Bump ``dependabot/fetch-metadata`` from 1.6.0 to 2.0.0 (:pr:`4185`) + + Version 21.0.1 ============== diff --git a/README.rst b/README.rst index 8366a1ff6f0..1d6b20aafea 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.1-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -89,7 +89,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **7.1** are supported. +All types and methods of the Telegram Bot API **7.2** are supported. Installing ========== diff --git a/README_RAW.rst b/README_RAW.rst index fae3d516e38..df1312e4857 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.1-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -85,7 +85,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **7.1** are supported. +All types and methods of the Telegram Bot API **7.2** are supported. Installing ========== diff --git a/docs/source/conf.py b/docs/source/conf.py index 44bd4400cd5..372d0c81581 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,9 +20,9 @@ # built documents. # # The short X.Y version. -version = "21.0.1" # telegram.__version__[:3] +version = "21.1" # telegram.__version__[:3] # The full version, including alpha/beta/rc tags. -release = "21.0.1" # telegram.__version__ +release = "21.1" # telegram.__version__ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = "6.1.3" @@ -140,12 +140,6 @@ "admonition-title-font-size": "0.95rem", "admonition-font-size": "0.92rem", }, - "announcement": ( - "PTB has undergone significant changes in v20. Please read the documentation " - "carefully and also check out the transition guide in the " - 'wiki.' - ), "footer_icons": [ { # Telegram channel logo diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 1f05f11ff11..9dcfa1982e2 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -113,6 +113,10 @@ :align: left :widths: 1 4 + * - :meth:`~telegram.Bot.approve_chat_join_request` + - Used for approving a chat join request + * - :meth:`~telegram.Bot.decline_chat_join_request` + - Used for declining a chat join request * - :meth:`~telegram.Bot.ban_chat_member` - Used for banning a member from the chat * - :meth:`~telegram.Bot.unban_chat_member` @@ -137,10 +141,6 @@ - Used for editing a non-primary invite link * - :meth:`~telegram.Bot.revoke_chat_invite_link` - Used for revoking an invite link created by the bot - * - :meth:`~telegram.Bot.approve_chat_join_request` - - Used for approving a chat join request - * - :meth:`~telegram.Bot.decline_chat_join_request` - - Used for declining a chat join request * - :meth:`~telegram.Bot.set_chat_photo` - Used for setting a photo to a chat * - :meth:`~telegram.Bot.delete_chat_photo` @@ -155,6 +155,8 @@ - Used for unpinning a message * - :meth:`~telegram.Bot.unpin_all_chat_messages` - Used for unpinning all pinned chat messages + * - :meth:`~telegram.Bot.get_business_connection` + - Used for getting information about the business account. * - :meth:`~telegram.Bot.get_user_profile_photos` - Used for obtaining user's profile pictures * - :meth:`~telegram.Bot.get_chat` @@ -237,6 +239,8 @@ - Used for setting a sticker set of a chat * - :meth:`~telegram.Bot.delete_chat_sticker_set` - Used for deleting the set sticker set of a chat + * - :meth:`~telegram.Bot.replace_sticker_in_set` + - Used for replacing a sticker in a set * - :meth:`~telegram.Bot.set_sticker_position_in_set` - Used for moving a sticker's position in the set * - :meth:`~telegram.Bot.set_sticker_set_title` diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index ffa7107b89e..3d78292588a 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -6,6 +6,7 @@ Available Types telegram.animation telegram.audio + telegram.birthdate telegram.botcommand telegram.botcommandscope telegram.botcommandscopeallchatadministrators @@ -18,6 +19,12 @@ Available Types telegram.botdescription telegram.botname telegram.botshortdescription + telegram.businessconnection + telegram.businessintro + telegram.businesslocation + telegram.businessopeninghours + telegram.businessopeninghoursinterval + telegram.businessmessagesdeleted telegram.callbackquery telegram.chat telegram.chatadministratorrights @@ -107,6 +114,7 @@ Available Types telegram.replykeyboardremove telegram.replyparameters telegram.sentwebappmessage + telegram.shareduser telegram.story telegram.switchinlinequerychosenchat telegram.telegramobject diff --git a/docs/source/telegram.birthdate.rst b/docs/source/telegram.birthdate.rst new file mode 100644 index 00000000000..083de5ebf4a --- /dev/null +++ b/docs/source/telegram.birthdate.rst @@ -0,0 +1,7 @@ +Birthdate +========= + +.. autoclass:: telegram.Birthdate + :members: + :show-inheritance: + diff --git a/docs/source/telegram.businessconnection.rst b/docs/source/telegram.businessconnection.rst new file mode 100644 index 00000000000..3ef31c3b25e --- /dev/null +++ b/docs/source/telegram.businessconnection.rst @@ -0,0 +1,6 @@ +BusinessConnection +================== + +.. autoclass:: telegram.BusinessConnection + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.businessintro.rst b/docs/source/telegram.businessintro.rst new file mode 100644 index 00000000000..4870258e5b4 --- /dev/null +++ b/docs/source/telegram.businessintro.rst @@ -0,0 +1,6 @@ +BusinessIntro +================== + +.. autoclass:: telegram.BusinessIntro + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.businesslocation.rst b/docs/source/telegram.businesslocation.rst new file mode 100644 index 00000000000..1a1b8893b65 --- /dev/null +++ b/docs/source/telegram.businesslocation.rst @@ -0,0 +1,6 @@ +BusinessLocation +================== + +.. autoclass:: telegram.BusinessLocation + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.businessmessagesdeleted.rst b/docs/source/telegram.businessmessagesdeleted.rst new file mode 100644 index 00000000000..ba0e88e3cba --- /dev/null +++ b/docs/source/telegram.businessmessagesdeleted.rst @@ -0,0 +1,6 @@ +BusinessMessagesDeleted +======================= + +.. autoclass:: telegram.BusinessMessagesDeleted + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.businessopeninghours.rst b/docs/source/telegram.businessopeninghours.rst new file mode 100644 index 00000000000..cab989c8475 --- /dev/null +++ b/docs/source/telegram.businessopeninghours.rst @@ -0,0 +1,6 @@ +BusinessOpeningHours +==================== + +.. autoclass:: telegram.BusinessOpeningHours + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.businessopeninghoursinterval.rst b/docs/source/telegram.businessopeninghoursinterval.rst new file mode 100644 index 00000000000..241379dbcfb --- /dev/null +++ b/docs/source/telegram.businessopeninghoursinterval.rst @@ -0,0 +1,6 @@ +BusinessOpeningHoursInterval +============================ + +.. autoclass:: telegram.BusinessOpeningHoursInterval + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.ext.businessconnectionhandler.rst b/docs/source/telegram.ext.businessconnectionhandler.rst new file mode 100644 index 00000000000..0b0509dff2f --- /dev/null +++ b/docs/source/telegram.ext.businessconnectionhandler.rst @@ -0,0 +1,6 @@ +BusinessConnectionHandler +========================= + +.. autoclass:: telegram.ext.BusinessConnectionHandler + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.ext.businessmessagesdeletedhandler.rst b/docs/source/telegram.ext.businessmessagesdeletedhandler.rst new file mode 100644 index 00000000000..840f19325a0 --- /dev/null +++ b/docs/source/telegram.ext.businessmessagesdeletedhandler.rst @@ -0,0 +1,6 @@ +BusinessMessagesDeletedHandler +============================== + +.. autoclass:: telegram.ext.BusinessMessagesDeletedHandler + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.ext.handlers-tree.rst b/docs/source/telegram.ext.handlers-tree.rst index e5df80b2cc6..6749cacb9dd 100644 --- a/docs/source/telegram.ext.handlers-tree.rst +++ b/docs/source/telegram.ext.handlers-tree.rst @@ -5,6 +5,8 @@ Handlers :titlesonly: telegram.ext.basehandler + telegram.ext.businessconnectionhandler + telegram.ext.businessmessagesdeletedhandler telegram.ext.callbackqueryhandler telegram.ext.chatboosthandler telegram.ext.chatjoinrequesthandler diff --git a/docs/source/telegram.shareduser.rst b/docs/source/telegram.shareduser.rst new file mode 100644 index 00000000000..52dd3885bc0 --- /dev/null +++ b/docs/source/telegram.shareduser.rst @@ -0,0 +1,7 @@ +SharedUser +========== + +.. autoclass:: telegram.SharedUser + :members: + :show-inheritance: + diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index 050a6d52b9e..36038e71eba 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -79,3 +79,5 @@ .. |do_quote| replace:: If set to :obj:`True`, the replied message is quoted. For a dict, it must be the output of :meth:`~telegram.Message.build_reply_arguments` to specify exact ``reply_parameters``. If ``reply_to_message_id`` or ``reply_parameters`` are passed, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. .. |non_optional_story_argument| replace:: As of this version, this argument is now required. In accordance with our `stability policy `__, the signature will be kept as optional for now, though they are mandatory and an error will be raised if you don't pass it. + +.. |business_id_str| replace:: Unique identifier of the business connection on behalf of which the message will be sent. diff --git a/pyproject.toml b/pyproject.toml index b941a244486..34c2a763798 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ explicit-preview-rules = true ignore = ["PLR2004", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PERF203"] select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE", "G", "ISC", "PT", "ASYNC", "TCH", "SLOT", "PERF", "PYI", "FLY", "AIR", "RUF022", - "RUF023", "Q", "INP",] + "RUF023", "Q", "INP", "W"] # Add "FURB" after it's out of preview [tool.ruff.lint.per-file-ignores] diff --git a/telegram/__init__.py b/telegram/__init__.py index 162ba3d0edb..304425c4d61 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -22,6 +22,7 @@ __all__ = ( "Animation", "Audio", + "Birthdate", "Bot", "BotCommand", "BotCommandScope", @@ -35,6 +36,12 @@ "BotDescription", "BotName", "BotShortDescription", + "BusinessConnection", + "BusinessIntro", + "BusinessLocation", + "BusinessMessagesDeleted", + "BusinessOpeningHours", + "BusinessOpeningHoursInterval", "CallbackGame", "CallbackQuery", "Chat", @@ -184,6 +191,7 @@ "SecureData", "SecureValue", "SentWebAppMessage", + "SharedUser", "ShippingAddress", "ShippingOption", "ShippingQuery", @@ -224,6 +232,7 @@ from . import _version, constants, error, helpers, request, warnings +from ._birthdate import Birthdate from ._bot import Bot from ._botcommand import BotCommand from ._botcommandscope import ( @@ -238,6 +247,14 @@ ) from ._botdescription import BotDescription, BotShortDescription from ._botname import BotName +from ._business import ( + BusinessConnection, + BusinessIntro, + BusinessLocation, + BusinessMessagesDeleted, + BusinessOpeningHours, + BusinessOpeningHoursInterval, +) from ._callbackquery import CallbackQuery from ._chat import Chat from ._chatadministratorrights import ChatAdministratorRights @@ -393,7 +410,7 @@ from ._replykeyboardmarkup import ReplyKeyboardMarkup from ._replykeyboardremove import ReplyKeyboardRemove from ._sentwebappmessage import SentWebAppMessage -from ._shared import ChatShared, UsersShared +from ._shared import ChatShared, SharedUser, UsersShared from ._story import Story from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject diff --git a/telegram/_birthdate.py b/telegram/_birthdate.py new file mode 100644 index 00000000000..23c3ebc4764 --- /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:: 21.1 + + 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 df498eb7a3e..8bb4af23de7 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 @@ -21,7 +21,6 @@ import asyncio import contextlib import copy -import functools import pickle from datetime import datetime from types import TracebackType @@ -58,6 +57,7 @@ from telegram._botcommandscope import BotCommandScope from telegram._botdescription import BotDescription, BotShortDescription from telegram._botname import BotName +from telegram._business import BusinessConnection from telegram._chat import Chat from telegram._chatadministratorrights import ChatAdministratorRights from telegram._chatboost import UserChatBoosts @@ -531,20 +531,6 @@ def _warn( """ warn(message=message, category=category, stacklevel=stacklevel + 1) - # TODO: After https://youtrack.jetbrains.com/issue/PY-50952 is fixed, we can revisit this and - # consider adding Paramspec from typing_extensions to properly fix this. Currently a workaround - def _log(func: Any): # type: ignore[no-untyped-def] # skipcq: PY-D0003 - @functools.wraps(func) - async def decorator(self: "Bot", *args: Any, **kwargs: Any) -> Any: - # pylint: disable=protected-access - self._LOGGER.debug("Entering: %s", func.__name__) - result = await func(self, *args, **kwargs) - self._LOGGER.debug(result) - self._LOGGER.debug("Exiting: %s", func.__name__) - return result - - return decorator - def _parse_file_input( self, file_input: Union[FileInput, "TelegramObject"], @@ -654,7 +640,8 @@ async def _do_post( request = self._request[0] if endpoint == "getUpdates" else self._request[1] - return await request.post( + self._LOGGER.debug("Calling Bot API endpoint `%s` with parameters `%s`", endpoint, data) + result = await request.post( url=f"{self._base_url}/{endpoint}", request_data=request_data, read_timeout=read_timeout, @@ -662,6 +649,11 @@ async def _do_post( connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) + self._LOGGER.debug( + "Call to Bot API endpoint `%s` finished with return value `%s`", endpoint, result + ) + + return result async def _send_message( self, @@ -676,6 +668,7 @@ async def _send_message( caption_entities: Optional[Sequence["MessageEntity"]] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -732,6 +725,9 @@ async def _send_message( if caption_entities is not None: data["caption_entities"] = caption_entities + if business_connection_id is not None: + data["business_connection_id"] = business_connection_id + result = await self._post( endpoint, data, @@ -784,7 +780,6 @@ async def shutdown(self) -> None: await asyncio.gather(self._request[0].shutdown(), self._request[1].shutdown()) self._initialized = False - @_log async def do_api_request( self, endpoint: str, @@ -843,7 +838,7 @@ async def do_api_request( f"'Bot.do_api_request(\"{endpoint}\", ...)'" ), PTBDeprecationWarning, - stacklevel=3, + stacklevel=2, ) camel_case_endpoint = to_camel_case(endpoint) @@ -879,7 +874,6 @@ async def do_api_request( return return_type.de_list(result, self) return return_type.de_json(result, self) - @_log async def get_me( self, *, @@ -910,7 +904,6 @@ async def get_me( self._bot_user = User.de_json(result, self) return self._bot_user # type: ignore[return-value] - @_log async def send_message( self, chat_id: Union[int, str], @@ -923,6 +916,7 @@ async def send_message( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -968,6 +962,9 @@ async def send_message( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1021,6 +1018,7 @@ async def send_message( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, parse_mode=parse_mode, link_preview_options=link_preview_options, reply_parameters=reply_parameters, @@ -1031,7 +1029,6 @@ async def send_message( api_kwargs=api_kwargs, ) - @_log async def delete_message( self, chat_id: Union[str, int], @@ -1090,7 +1087,6 @@ async def delete_message( api_kwargs=api_kwargs, ) - @_log async def delete_messages( self, chat_id: Union[int, str], @@ -1133,7 +1129,6 @@ async def delete_messages( api_kwargs=api_kwargs, ) - @_log async def forward_message( self, chat_id: Union[int, str], @@ -1199,7 +1194,6 @@ async def forward_message( api_kwargs=api_kwargs, ) - @_log async def forward_messages( self, chat_id: Union[int, str], @@ -1262,7 +1256,6 @@ async def forward_messages( ) return MessageId.de_list(result, self) - @_log async def send_photo( self, chat_id: Union[int, str], @@ -1276,6 +1269,7 @@ async def send_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1336,6 +1330,9 @@ async def send_photo( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1393,9 +1390,9 @@ async def send_photo( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_audio( self, chat_id: Union[int, str], @@ -1412,6 +1409,7 @@ async def send_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1481,6 +1479,9 @@ async def send_audio( reply_parameters (:obj:`ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1541,9 +1542,9 @@ async def send_audio( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_document( self, chat_id: Union[int, str], @@ -1558,6 +1559,7 @@ async def send_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1626,6 +1628,9 @@ async def send_document( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1682,9 +1687,9 @@ async def send_document( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_sticker( self, chat_id: Union[int, str], @@ -1695,6 +1700,7 @@ async def send_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1713,8 +1719,8 @@ async def send_sticker( chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Sticker`): Sticker to send. - |fileinput| Video stickers can only be sent by a ``file_id``. Animated stickers - can't be sent via an HTTP URL. + |fileinput| Video stickers can only be sent by a ``file_id``. Video and animated + stickers can't be sent via an HTTP URL. Lastly you can pass an existing :class:`telegram.Sticker` object to send. @@ -1743,6 +1749,9 @@ async def send_sticker( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1791,9 +1800,9 @@ async def send_sticker( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_video( self, chat_id: Union[int, str], @@ -1812,6 +1821,7 @@ async def send_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1889,6 +1899,9 @@ async def send_video( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1951,9 +1964,9 @@ async def send_video( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_video_note( self, chat_id: Union[int, str], @@ -1966,6 +1979,7 @@ async def send_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2028,6 +2042,9 @@ async def send_video_note( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2084,9 +2101,9 @@ async def send_video_note( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_animation( self, chat_id: Union[int, str], @@ -2104,6 +2121,7 @@ async def send_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2175,6 +2193,9 @@ async def send_animation( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2236,9 +2257,9 @@ async def send_animation( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_voice( self, chat_id: Union[int, str], @@ -2252,6 +2273,7 @@ async def send_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2316,6 +2338,9 @@ async def send_voice( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2373,9 +2398,9 @@ async def send_voice( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_media_group( self, chat_id: Union[int, str], @@ -2386,6 +2411,7 @@ async def send_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2433,6 +2459,9 @@ async def send_media_group( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2525,6 +2554,7 @@ async def send_media_group( "protect_content": protect_content, "message_thread_id": message_thread_id, "reply_parameters": reply_parameters, + "business_connection_id": business_connection_id, } result = await self._post( @@ -2539,7 +2569,6 @@ async def send_media_group( return Message.de_list(result, self) - @_log async def send_location( self, chat_id: Union[int, str], @@ -2554,6 +2583,7 @@ async def send_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2605,6 +2635,9 @@ async def send_location( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2673,9 +2706,9 @@ async def send_location( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def edit_message_live_location( self, chat_id: Optional[Union[str, int]] = None, @@ -2770,7 +2803,6 @@ async def edit_message_live_location( api_kwargs=api_kwargs, ) - @_log async def stop_message_live_location( self, chat_id: Optional[Union[str, int]] = None, @@ -2818,7 +2850,6 @@ async def stop_message_live_location( api_kwargs=api_kwargs, ) - @_log async def send_venue( self, chat_id: Union[int, str], @@ -2835,6 +2866,7 @@ async def send_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2884,6 +2916,9 @@ async def send_venue( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2963,9 +2998,9 @@ async def send_venue( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_contact( self, chat_id: Union[int, str], @@ -2978,6 +3013,7 @@ async def send_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3017,6 +3053,9 @@ async def send_contact( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -3087,9 +3126,9 @@ async def send_contact( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_game( self, chat_id: int, @@ -3099,6 +3138,7 @@ async def send_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3128,6 +3168,9 @@ async def send_game( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -3173,14 +3216,15 @@ async def send_game( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def send_chat_action( self, 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, @@ -3202,6 +3246,9 @@ async def send_chat_action( message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -3214,6 +3261,7 @@ async def send_chat_action( "chat_id": chat_id, "action": action, "message_thread_id": message_thread_id, + "business_connection_id": business_connection_id, } return await self._post( "sendChatAction", @@ -3319,7 +3367,6 @@ def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQ return res - @_log async def answer_inline_query( self, inline_query_id: str, @@ -3417,7 +3464,6 @@ async def answer_inline_query( api_kwargs=api_kwargs, ) - @_log async def get_user_profile_photos( self, user_id: int, @@ -3462,7 +3508,6 @@ async def get_user_profile_photos( return UserProfilePhotos.de_json(result, self) # type: ignore[return-value] - @_log async def get_file( self, file_id: Union[ @@ -3528,7 +3573,6 @@ async def get_file( return File.de_json(result, self) # type: ignore[return-value] - @_log async def ban_chat_member( self, chat_id: Union[str, int], @@ -3592,7 +3636,6 @@ async def ban_chat_member( api_kwargs=api_kwargs, ) - @_log async def ban_chat_sender_chat( self, chat_id: Union[str, int], @@ -3636,7 +3679,6 @@ async def ban_chat_sender_chat( api_kwargs=api_kwargs, ) - @_log async def unban_chat_member( self, chat_id: Union[str, int], @@ -3681,7 +3723,6 @@ async def unban_chat_member( api_kwargs=api_kwargs, ) - @_log async def unban_chat_sender_chat( self, chat_id: Union[str, int], @@ -3722,7 +3763,6 @@ async def unban_chat_sender_chat( api_kwargs=api_kwargs, ) - @_log async def answer_callback_query( self, callback_query_id: str, @@ -3788,7 +3828,6 @@ async def answer_callback_query( api_kwargs=api_kwargs, ) - @_log async def edit_message_text( self, text: str, @@ -3890,7 +3929,6 @@ async def edit_message_text( api_kwargs=api_kwargs, ) - @_log async def edit_message_caption( self, chat_id: Optional[Union[str, int]] = None, @@ -3960,7 +3998,6 @@ async def edit_message_caption( api_kwargs=api_kwargs, ) - @_log async def edit_message_media( self, media: "InputMedia", @@ -4024,7 +4061,6 @@ async def edit_message_media( api_kwargs=api_kwargs, ) - @_log async def edit_message_reply_markup( self, chat_id: Optional[Union[str, int]] = None, @@ -4080,7 +4116,6 @@ async def edit_message_reply_markup( api_kwargs=api_kwargs, ) - @_log async def get_updates( self, offset: Optional[int] = None, @@ -4164,7 +4199,7 @@ async def get_updates( "the property `read_timeout`. Overriding this property will be mandatory in " "future versions. Using 2 seconds as fallback.", PTBDeprecationWarning, - stacklevel=3, + stacklevel=2, ) # Ideally we'd use an aggressive read timeout for the polling. However, @@ -4192,7 +4227,6 @@ async def get_updates( return Update.de_list(result, self) - @_log async def set_webhook( self, url: str, @@ -4320,7 +4354,6 @@ async def set_webhook( api_kwargs=api_kwargs, ) - @_log async def delete_webhook( self, drop_pending_updates: Optional[bool] = None, @@ -4358,7 +4391,6 @@ async def delete_webhook( api_kwargs=api_kwargs, ) - @_log async def leave_chat( self, chat_id: Union[str, int], @@ -4393,7 +4425,6 @@ async def leave_chat( api_kwargs=api_kwargs, ) - @_log async def get_chat( self, chat_id: Union[str, int], @@ -4432,7 +4463,6 @@ async def get_chat( return Chat.de_json(result, self) # type: ignore[return-value] - @_log async def get_chat_administrators( self, chat_id: Union[str, int], @@ -4474,7 +4504,6 @@ async def get_chat_administrators( ) return ChatMember.de_list(result, self) - @_log async def get_chat_member_count( self, chat_id: Union[str, int], @@ -4510,7 +4539,6 @@ async def get_chat_member_count( api_kwargs=api_kwargs, ) - @_log async def get_chat_member( self, chat_id: Union[str, int], @@ -4548,7 +4576,6 @@ async def get_chat_member( ) return ChatMember.de_json(result, self) # type: ignore[return-value] - @_log async def set_chat_sticker_set( self, chat_id: Union[str, int], @@ -4584,7 +4611,6 @@ async def set_chat_sticker_set( api_kwargs=api_kwargs, ) - @_log async def delete_chat_sticker_set( self, chat_id: Union[str, int], @@ -4617,7 +4643,6 @@ async def delete_chat_sticker_set( api_kwargs=api_kwargs, ) - @_log async def get_webhook_info( self, *, @@ -4646,7 +4671,6 @@ async def get_webhook_info( ) return WebhookInfo.de_json(result, self) # type: ignore[return-value] - @_log async def set_game_score( self, user_id: int, @@ -4711,7 +4735,6 @@ async def set_game_score( api_kwargs=api_kwargs, ) - @_log async def get_game_high_scores( self, user_id: int, @@ -4772,7 +4795,6 @@ async def get_game_high_scores( return GameHighScore.de_list(result, self) - @_log async def send_invoice( self, chat_id: Union[int, str], @@ -4977,7 +4999,6 @@ async def send_invoice( api_kwargs=api_kwargs, ) - @_log async def answer_shipping_query( self, shipping_query_id: str, @@ -5036,7 +5057,6 @@ async def answer_shipping_query( api_kwargs=api_kwargs, ) - @_log async def answer_pre_checkout_query( self, pre_checkout_query_id: str, @@ -5093,7 +5113,6 @@ async def answer_pre_checkout_query( api_kwargs=api_kwargs, ) - @_log async def answer_web_app_query( self, web_app_query_id: str, @@ -5140,7 +5159,6 @@ async def answer_web_app_query( return SentWebAppMessage.de_json(api_result, self) # type: ignore[return-value] - @_log async def restrict_chat_member( self, chat_id: Union[str, int], @@ -5214,7 +5232,6 @@ async def restrict_chat_member( api_kwargs=api_kwargs, ) - @_log async def promote_chat_member( self, chat_id: Union[str, int], @@ -5339,7 +5356,6 @@ async def promote_chat_member( api_kwargs=api_kwargs, ) - @_log async def set_chat_permissions( self, chat_id: Union[str, int], @@ -5398,7 +5414,6 @@ async def set_chat_permissions( api_kwargs=api_kwargs, ) - @_log async def set_chat_administrator_custom_title( self, chat_id: Union[int, str], @@ -5441,7 +5456,6 @@ async def set_chat_administrator_custom_title( api_kwargs=api_kwargs, ) - @_log async def export_chat_invite_link( self, chat_id: Union[str, int], @@ -5485,7 +5499,6 @@ async def export_chat_invite_link( api_kwargs=api_kwargs, ) - @_log async def create_chat_invite_link( self, chat_id: Union[str, int], @@ -5563,7 +5576,6 @@ async def create_chat_invite_link( return ChatInviteLink.de_json(result, self) # type: ignore[return-value] - @_log async def edit_chat_invite_link( self, chat_id: Union[str, int], @@ -5645,7 +5657,6 @@ async def edit_chat_invite_link( return ChatInviteLink.de_json(result, self) # type: ignore[return-value] - @_log async def revoke_chat_invite_link( self, chat_id: Union[str, int], @@ -5693,7 +5704,6 @@ async def revoke_chat_invite_link( return ChatInviteLink.de_json(result, self) # type: ignore[return-value] - @_log async def approve_chat_join_request( self, chat_id: Union[str, int], @@ -5734,7 +5744,6 @@ async def approve_chat_join_request( api_kwargs=api_kwargs, ) - @_log async def decline_chat_join_request( self, chat_id: Union[str, int], @@ -5775,7 +5784,6 @@ async def decline_chat_join_request( api_kwargs=api_kwargs, ) - @_log async def set_chat_photo( self, chat_id: Union[str, int], @@ -5822,7 +5830,6 @@ async def set_chat_photo( api_kwargs=api_kwargs, ) - @_log async def delete_chat_photo( self, chat_id: Union[str, int], @@ -5859,7 +5866,6 @@ async def delete_chat_photo( api_kwargs=api_kwargs, ) - @_log async def set_chat_title( self, chat_id: Union[str, int], @@ -5900,7 +5906,6 @@ async def set_chat_title( api_kwargs=api_kwargs, ) - @_log async def set_chat_description( self, chat_id: Union[str, int], @@ -5942,7 +5947,6 @@ async def set_chat_description( api_kwargs=api_kwargs, ) - @_log async def pin_chat_message( self, chat_id: Union[str, int], @@ -5992,7 +5996,6 @@ async def pin_chat_message( api_kwargs=api_kwargs, ) - @_log async def unpin_chat_message( self, chat_id: Union[str, int], @@ -6035,7 +6038,6 @@ async def unpin_chat_message( api_kwargs=api_kwargs, ) - @_log async def unpin_all_chat_messages( self, chat_id: Union[str, int], @@ -6074,7 +6076,6 @@ async def unpin_all_chat_messages( api_kwargs=api_kwargs, ) - @_log async def get_sticker_set( self, name: str, @@ -6109,7 +6110,6 @@ async def get_sticker_set( ) return StickerSet.de_json(result, self) # type: ignore[return-value] - @_log async def get_custom_emoji_stickers( self, custom_emoji_ids: Sequence[str], @@ -6153,7 +6153,6 @@ async def get_custom_emoji_stickers( ) return Sticker.de_list(result, self) - @_log async def upload_sticker_file( self, user_id: int, @@ -6213,7 +6212,6 @@ async def upload_sticker_file( ) return File.de_json(result, self) # type: ignore[return-value] - @_log async def add_sticker_to_set( self, user_id: int, @@ -6229,9 +6227,7 @@ async def add_sticker_to_set( """ Use this method to add a new sticker to a set created by the bot. The format of the added sticker must match the format of the other stickers in the set. Emoji sticker sets can have - up to :tg-const:`telegram.constants.StickerSetLimit.MAX_EMOJI_STICKERS` stickers. Animated - and video sticker sets can have up to - :tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_STICKERS` stickers. Static + up to :tg-const:`telegram.constants.StickerSetLimit.MAX_EMOJI_STICKERS` stickers. Other sticker sets can have up to :tg-const:`telegram.constants.StickerSetLimit.MAX_STATIC_STICKERS` stickers. @@ -6275,7 +6271,6 @@ async def add_sticker_to_set( api_kwargs=api_kwargs, ) - @_log async def set_sticker_position_in_set( self, sticker: str, @@ -6311,14 +6306,13 @@ async def set_sticker_position_in_set( api_kwargs=api_kwargs, ) - @_log async def create_new_sticker_set( self, user_id: int, 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, *, @@ -6371,6 +6365,9 @@ async def create_new_sticker_set( .. versionadded:: 20.2 + .. deprecated:: 21.1 + 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 @@ -6390,6 +6387,14 @@ async def create_new_sticker_set( Raises: :class:`telegram.error.TelegramError` """ + if sticker_format is not None: + warn( + "The parameter `sticker_format` is deprecated. Use the parameter" + " `InputSticker.format` in the parameter `stickers` instead.", + stacklevel=2, + category=PTBDeprecationWarning, + ) + data: JSONDict = { "user_id": user_id, "name": name, @@ -6410,7 +6415,6 @@ async def create_new_sticker_set( api_kwargs=api_kwargs, ) - @_log async def delete_sticker_from_set( self, sticker: str, @@ -6444,7 +6448,6 @@ async def delete_sticker_from_set( api_kwargs=api_kwargs, ) - @_log async def delete_sticker_set( self, name: str, @@ -6481,11 +6484,11 @@ async def delete_sticker_set( api_kwargs=api_kwargs, ) - @_log async def set_sticker_set_thumbnail( self, name: str, user_id: int, + format: str, # pylint: disable=redefined-builtin thumbnail: Optional[FileInput] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -6499,9 +6502,21 @@ async def set_sticker_set_thumbnail( .. versionadded:: 20.2 + .. versionchanged:: 21.1 + 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:: 21.1 + 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` @@ -6534,6 +6549,7 @@ async def set_sticker_set_thumbnail( "name": name, "user_id": user_id, "thumbnail": self._parse_file_input(thumbnail) if thumbnail else None, + "format": format, } return await self._post( @@ -6546,7 +6562,6 @@ async def set_sticker_set_thumbnail( api_kwargs=api_kwargs, ) - @_log async def set_sticker_set_title( self, name: str, @@ -6587,7 +6602,6 @@ async def set_sticker_set_title( api_kwargs=api_kwargs, ) - @_log async def set_sticker_emoji_list( self, sticker: str, @@ -6629,7 +6643,6 @@ async def set_sticker_emoji_list( api_kwargs=api_kwargs, ) - @_log async def set_sticker_keywords( self, sticker: str, @@ -6671,7 +6684,6 @@ async def set_sticker_keywords( api_kwargs=api_kwargs, ) - @_log async def set_sticker_mask_position( self, sticker: str, @@ -6712,7 +6724,6 @@ async def set_sticker_mask_position( api_kwargs=api_kwargs, ) - @_log async def set_custom_emoji_sticker_set_thumbnail( self, name: str, @@ -6754,7 +6765,6 @@ async def set_custom_emoji_sticker_set_thumbnail( api_kwargs=api_kwargs, ) - @_log async def set_passport_data_errors( self, user_id: int, @@ -6801,7 +6811,6 @@ async def set_passport_data_errors( api_kwargs=api_kwargs, ) - @_log async def send_poll( self, chat_id: Union[int, str], @@ -6822,6 +6831,7 @@ async def send_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -6897,6 +6907,9 @@ async def send_poll( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -6956,9 +6969,9 @@ async def send_poll( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def stop_poll( self, chat_id: Union[int, str], @@ -7004,7 +7017,6 @@ async def stop_poll( ) return Poll.de_json(result, self) # type: ignore[return-value] - @_log async def send_dice( self, chat_id: Union[int, str], @@ -7014,6 +7026,7 @@ async def send_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -7057,6 +7070,9 @@ async def send_dice( reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -7103,9 +7119,9 @@ async def send_dice( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) - @_log async def get_my_default_administrator_rights( self, for_channels: Optional[bool] = None, @@ -7147,7 +7163,6 @@ async def get_my_default_administrator_rights( return ChatAdministratorRights.de_json(result, self) # type: ignore[return-value] - @_log async def set_my_default_administrator_rights( self, rights: Optional[ChatAdministratorRights] = None, @@ -7161,7 +7176,7 @@ async def set_my_default_administrator_rights( ) -> bool: """Use this method to change the default administrator rights requested by the bot when it's added as an administrator to groups or channels. These rights will be suggested to - users, but they are are free to modify the list before adding the bot. + users, but they are free to modify the list before adding the bot. .. seealso:: :meth:`get_my_default_administrator_rights` @@ -7193,7 +7208,6 @@ async def set_my_default_administrator_rights( api_kwargs=api_kwargs, ) - @_log async def get_my_commands( self, scope: Optional[BotCommandScope] = None, @@ -7247,7 +7261,6 @@ async def get_my_commands( return BotCommand.de_list(result, self) - @_log async def set_my_commands( self, commands: Sequence[Union[BotCommand, Tuple[str, str]]], @@ -7312,7 +7325,6 @@ async def set_my_commands( api_kwargs=api_kwargs, ) - @_log async def delete_my_commands( self, scope: Optional[BotCommandScope] = None, @@ -7360,7 +7372,6 @@ async def delete_my_commands( api_kwargs=api_kwargs, ) - @_log async def log_out( self, *, @@ -7393,7 +7404,6 @@ async def log_out( api_kwargs=api_kwargs, ) - @_log async def close( self, *, @@ -7425,7 +7435,6 @@ async def close( api_kwargs=api_kwargs, ) - @_log async def copy_message( self, chat_id: Union[int, str], @@ -7551,7 +7560,6 @@ async def copy_message( ) return MessageId.de_json(result, self) # type: ignore[return-value] - @_log async def copy_messages( self, chat_id: Union[int, str], @@ -7622,7 +7630,6 @@ async def copy_messages( ) return MessageId.de_list(result, self) - @_log async def set_chat_menu_button( self, chat_id: Optional[int] = None, @@ -7664,7 +7671,6 @@ async def set_chat_menu_button( api_kwargs=api_kwargs, ) - @_log async def get_chat_menu_button( self, chat_id: Optional[int] = None, @@ -7704,7 +7710,6 @@ async def get_chat_menu_button( ) return MenuButton.de_json(result, bot=self) # type: ignore[return-value] - @_log async def create_invoice_link( self, title: str, @@ -7833,7 +7838,6 @@ async def create_invoice_link( api_kwargs=api_kwargs, ) - @_log async def get_forum_topic_icon_stickers( self, *, @@ -7865,7 +7869,6 @@ async def get_forum_topic_icon_stickers( ) return Sticker.de_list(result, self) - @_log async def create_forum_topic( self, chat_id: Union[str, int], @@ -7925,7 +7928,6 @@ async def create_forum_topic( ) return ForumTopic.de_json(result, self) # type: ignore[return-value] - @_log async def edit_forum_topic( self, chat_id: Union[str, int], @@ -7982,7 +7984,6 @@ async def edit_forum_topic( api_kwargs=api_kwargs, ) - @_log async def close_forum_topic( self, chat_id: Union[str, int], @@ -8027,7 +8028,6 @@ async def close_forum_topic( api_kwargs=api_kwargs, ) - @_log async def reopen_forum_topic( self, chat_id: Union[str, int], @@ -8072,7 +8072,6 @@ async def reopen_forum_topic( api_kwargs=api_kwargs, ) - @_log async def delete_forum_topic( self, chat_id: Union[str, int], @@ -8116,7 +8115,6 @@ async def delete_forum_topic( api_kwargs=api_kwargs, ) - @_log async def unpin_all_forum_topic_messages( self, chat_id: Union[str, int], @@ -8161,7 +8159,6 @@ async def unpin_all_forum_topic_messages( api_kwargs=api_kwargs, ) - @_log async def unpin_all_general_forum_topic_messages( self, chat_id: Union[str, int], @@ -8201,7 +8198,6 @@ async def unpin_all_general_forum_topic_messages( api_kwargs=api_kwargs, ) - @_log async def edit_general_forum_topic( self, chat_id: Union[str, int], @@ -8245,7 +8241,6 @@ async def edit_general_forum_topic( api_kwargs=api_kwargs, ) - @_log async def close_general_forum_topic( self, chat_id: Union[str, int], @@ -8285,7 +8280,6 @@ async def close_general_forum_topic( api_kwargs=api_kwargs, ) - @_log async def reopen_general_forum_topic( self, chat_id: Union[str, int], @@ -8326,7 +8320,6 @@ async def reopen_general_forum_topic( api_kwargs=api_kwargs, ) - @_log async def hide_general_forum_topic( self, chat_id: Union[str, int], @@ -8367,7 +8360,6 @@ async def hide_general_forum_topic( api_kwargs=api_kwargs, ) - @_log async def unhide_general_forum_topic( self, chat_id: Union[str, int], @@ -8407,7 +8399,6 @@ async def unhide_general_forum_topic( api_kwargs=api_kwargs, ) - @_log async def set_my_description( self, description: Optional[str] = None, @@ -8453,7 +8444,6 @@ async def set_my_description( api_kwargs=api_kwargs, ) - @_log async def set_my_short_description( self, short_description: Optional[str] = None, @@ -8499,7 +8489,6 @@ async def set_my_short_description( api_kwargs=api_kwargs, ) - @_log async def get_my_description( self, language_code: Optional[str] = None, @@ -8538,7 +8527,6 @@ async def get_my_description( bot=self, ) - @_log async def get_my_short_description( self, language_code: Optional[str] = None, @@ -8578,7 +8566,6 @@ async def get_my_short_description( bot=self, ) - @_log async def set_my_name( self, name: Optional[str] = None, @@ -8627,7 +8614,6 @@ async def set_my_name( api_kwargs=api_kwargs, ) - @_log async def get_my_name( self, language_code: Optional[str] = None, @@ -8666,7 +8652,6 @@ async def get_my_name( bot=self, ) - @_log async def get_user_chat_boosts( self, chat_id: Union[str, int], @@ -8709,7 +8694,6 @@ async def get_user_chat_boosts( bot=self, ) - @_log async def set_message_reaction( self, chat_id: Union[str, int], @@ -8794,6 +8778,95 @@ async def set_message_reaction( api_kwargs=api_kwargs, ) + async def get_business_connection( + self, + business_connection_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> BusinessConnection: + """ + Use this method to get information about the connection of the bot with a business account. + + .. versionadded:: 21.1 + + 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:: 21.1 + + 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} @@ -9040,3 +9113,7 @@ def to_dict(self, recursive: bool = True) -> JSONDict: """Alias for :meth:`get_user_chat_boosts`""" setMessageReaction = set_message_reaction """Alias for :meth:`set_message_reaction`""" + getBusinessConnection = get_business_connection + """Alias for :meth:`get_business_connection`""" + replaceStickerInSet = replace_sticker_in_set + """Alias for :meth:`replace_sticker_in_set`""" diff --git a/telegram/_business.py b/telegram/_business.py new file mode 100644 index 00000000000..b15fd260b06 --- /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:: 21.1 + + 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:: 21.1 + + 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:: 21.1 + + 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:: 21.1 + + 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:: 21.1 + + 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:: 21.1 + + 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 c304be02def..94991c9b391 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple, Union from telegram import constants +from telegram._birthdate import Birthdate from telegram._chatlocation import ChatLocation from telegram._chatpermissions import ChatPermissions from telegram._files.chatphoto import ChatPhoto @@ -44,6 +45,9 @@ Animation, Audio, Bot, + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, ChatInviteLink, ChatMember, Contact, @@ -169,6 +173,21 @@ class Chat(TelegramObject): only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 + business_intro (:class:`telegram.BusinessIntro`, optional): For private chats with + business accounts, the intro of the business. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 21.1 + 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:: 21.1 + 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:: 21.1 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:: 21.1 + personal_chat (:obj:`telegram.Chat`, optional): For private chats, the personal channel of + the user. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 21.1 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:: 21.1 + 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:: 21.1 + 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:: 21.1 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:: 21.1 + personal_chat (:obj:`telegram.Chat`): Optional. For private chats, the personal channel of + the user. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 21.1 .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups .. _accent colors: https://core.telegram.org/bots/api#accent-colors @@ -383,6 +433,10 @@ class Chat(TelegramObject): "available_reactions", "background_custom_emoji_id", "bio", + "birthdate", + "business_intro", + "business_location", + "business_opening_hours", "can_set_sticker_set", "custom_emoji_sticker_set_name", "description", @@ -405,6 +459,7 @@ class Chat(TelegramObject): "location", "message_auto_delete_time", "permissions", + "personal_chat", "photo", "pinned_message", "profile_accent_color_id", @@ -470,6 +525,11 @@ def __init__( has_visible_history: Optional[bool] = None, unrestrict_boost_count: Optional[int] = None, custom_emoji_sticker_set_name: Optional[str] = None, + birthdate: Optional[Birthdate] = None, + personal_chat: Optional["Chat"] = None, + business_intro: Optional["BusinessIntro"] = None, + business_location: Optional["BusinessLocation"] = None, + business_opening_hours: Optional["BusinessOpeningHours"] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -519,6 +579,11 @@ def __init__( self.profile_background_custom_emoji_id: Optional[str] = profile_background_custom_emoji_id self.unrestrict_boost_count: Optional[int] = unrestrict_boost_count self.custom_emoji_sticker_set_name: Optional[str] = custom_emoji_sticker_set_name + self.birthdate: Optional[Birthdate] = birthdate + self.personal_chat: Optional["Chat"] = personal_chat + self.business_intro: Optional["BusinessIntro"] = business_intro + self.business_location: Optional["BusinessLocation"] = business_location + self.business_opening_hours: Optional["BusinessOpeningHours"] = business_opening_hours self._id_attrs = (self.id,) @@ -581,12 +646,24 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Chat"]: ) data["photo"] = ChatPhoto.de_json(data.get("photo"), bot) - from telegram import Message # pylint: disable=import-outside-toplevel + from telegram import ( # pylint: disable=import-outside-toplevel + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, + Message, + ) data["pinned_message"] = Message.de_json(data.get("pinned_message"), bot) data["permissions"] = ChatPermissions.de_json(data.get("permissions"), bot) data["location"] = ChatLocation.de_json(data.get("location"), bot) data["available_reactions"] = ReactionType.de_list(data.get("available_reactions"), bot) + data["birthdate"] = Birthdate.de_json(data.get("birthdate"), bot) + data["personal_chat"] = cls.de_json(data.get("personal_chat"), bot) + data["business_intro"] = BusinessIntro.de_json(data.get("business_intro"), bot) + data["business_location"] = BusinessLocation.de_json(data.get("business_location"), bot) + data["business_opening_hours"] = BusinessOpeningHours.de_json( + data.get("business_opening_hours"), bot + ) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility @@ -1169,7 +1246,7 @@ async def set_permissions( async def set_administrator_custom_title( self, - user_id: Union[int, str], + user_id: int, custom_title: str, *, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1444,6 +1521,7 @@ async def send_message( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1483,6 +1561,7 @@ async def send_message( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def delete_message( @@ -1558,6 +1637,7 @@ async def send_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1598,12 +1678,14 @@ async def send_media_group( parse_mode=parse_mode, caption_entities=caption_entities, reply_parameters=reply_parameters, + business_connection_id=business_connection_id, ) async def send_chat_action( self, action: str, message_thread_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1630,6 +1712,7 @@ async def send_chat_action( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) send_action = send_chat_action @@ -1647,6 +1730,7 @@ async def send_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1687,6 +1771,7 @@ async def send_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, has_spoiler=has_spoiler, + business_connection_id=business_connection_id, ) async def send_contact( @@ -1700,6 +1785,7 @@ async def send_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1739,6 +1825,7 @@ async def send_contact( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_audio( @@ -1756,6 +1843,7 @@ async def send_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1799,6 +1887,7 @@ async def send_audio( pool_timeout=pool_timeout, api_kwargs=api_kwargs, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_document( @@ -1814,6 +1903,7 @@ async def send_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1855,6 +1945,7 @@ async def send_document( caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_dice( @@ -1865,6 +1956,7 @@ async def send_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1899,6 +1991,7 @@ async def send_dice( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_game( @@ -1909,6 +2002,7 @@ async def send_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1943,6 +2037,7 @@ async def send_game( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_invoice( @@ -2052,6 +2147,7 @@ async def send_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2093,6 +2189,7 @@ async def send_location( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_animation( @@ -2111,6 +2208,7 @@ async def send_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2155,6 +2253,7 @@ async def send_animation( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_sticker( @@ -2166,6 +2265,7 @@ async def send_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2201,6 +2301,7 @@ async def send_sticker( protect_content=protect_content, message_thread_id=message_thread_id, emoji=emoji, + business_connection_id=business_connection_id, ) async def send_venue( @@ -2218,6 +2319,7 @@ async def send_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2261,6 +2363,7 @@ async def send_venue( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_video( @@ -2280,6 +2383,7 @@ async def send_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2325,6 +2429,7 @@ async def send_video( protect_content=protect_content, message_thread_id=message_thread_id, has_spoiler=has_spoiler, + business_connection_id=business_connection_id, ) async def send_video_note( @@ -2338,6 +2443,7 @@ async def send_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2377,6 +2483,7 @@ async def send_video_note( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_voice( @@ -2391,6 +2498,7 @@ async def send_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2431,6 +2539,7 @@ async def send_voice( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_poll( @@ -2452,6 +2561,7 @@ async def send_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2497,6 +2607,7 @@ async def send_poll( explanation_entities=explanation_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_copy( diff --git a/telegram/_chatadministratorrights.py b/telegram/_chatadministratorrights.py index fd6e45596dd..f2274fd8f9c 100644 --- a/telegram/_chatadministratorrights.py +++ b/telegram/_chatadministratorrights.py @@ -44,6 +44,11 @@ class ChatAdministratorRights(TelegramObject): :attr:`can_post_stories`, :attr:`can_edit_stories`, and :attr:`can_delete_stories` are considered as well when comparing objects of this type in terms of equality. + .. versionchanged:: 21.1 + As of this version, :attr:`can_post_stories`, :attr:`can_edit_stories`, + and :attr:`can_delete_stories` is now required. Thus, the order of arguments had to be + changed. + Args: is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event @@ -169,13 +174,13 @@ def __init__( can_promote_members: bool, can_change_info: bool, can_invite_users: bool, + can_post_stories: bool, + can_edit_stories: bool, + can_delete_stories: bool, can_post_messages: Optional[bool] = None, can_edit_messages: Optional[bool] = None, can_pin_messages: Optional[bool] = None, can_manage_topics: Optional[bool] = None, - can_post_stories: Optional[bool] = None, - can_edit_stories: Optional[bool] = None, - can_delete_stories: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: @@ -189,12 +194,6 @@ def __init__( self.can_promote_members: bool = can_promote_members self.can_change_info: bool = can_change_info self.can_invite_users: bool = can_invite_users - # Not actually optionals but because of backwards compatability we pretend they are - if can_post_stories is None or can_edit_stories is None or can_delete_stories is None: - raise TypeError( - "As of v21.0 can_post_stories, can_edit_stories and can_delete_stories" - " must be set in order to create this object." - ) self.can_post_stories: bool = can_post_stories self.can_edit_stories: bool = can_edit_stories self.can_delete_stories: bool = can_delete_stories diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index b42866b3a1d..20e28f4713b 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -191,6 +191,11 @@ class ChatMemberAdministrator(ChatMember): * The argument :paramref:`can_manage_topics` was added, which changes the position of the optional argument :paramref:`custom_title`. + .. versionchanged:: 21.1 + As of this version, :attr:`can_post_stories`, :attr:`can_edit_stories`, + and :attr:`can_delete_stories` is now required. Thus, the order of arguments had to be + changed. + Args: user (:class:`telegram.User`): Information about the user. can_be_edited (:obj:`bool`): :obj:`True`, if the bot @@ -340,14 +345,14 @@ def __init__( can_promote_members: bool, can_change_info: bool, can_invite_users: bool, + can_post_stories: bool, + can_edit_stories: bool, + can_delete_stories: bool, can_post_messages: Optional[bool] = None, can_edit_messages: Optional[bool] = None, can_pin_messages: Optional[bool] = None, can_manage_topics: Optional[bool] = None, custom_title: Optional[str] = None, - can_post_stories: Optional[bool] = None, - can_edit_stories: Optional[bool] = None, - can_delete_stories: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -362,12 +367,6 @@ def __init__( self.can_promote_members: bool = can_promote_members self.can_change_info: bool = can_change_info self.can_invite_users: bool = can_invite_users - # Not actually optionals but because of backwards compatability we pretend they are - if can_post_stories is None or can_edit_stories is None or can_delete_stories is None: - raise TypeError( - "As of 21.0 can_post_stories, can_edit_stories and can_delete_stories " - "must be set in order to create this object." - ) self.can_post_stories: bool = can_post_stories self.can_edit_stories: bool = can_edit_stories self.can_delete_stories: bool = can_delete_stories diff --git a/telegram/_files/inputsticker.py b/telegram/_files/inputsticker.py index bfcd89300a2..5539d610d83 100644 --- a/telegram/_files/inputsticker.py +++ b/telegram/_files/inputsticker.py @@ -36,6 +36,10 @@ class InputSticker(TelegramObject): .. versionadded:: 20.2 + .. versionchanged:: 21.1 + 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:: 21.1 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:: 21.1 """ - __slots__ = ("emoji_list", "keywords", "mask_position", "sticker") + __slots__ = ("emoji_list", "format", "keywords", "mask_position", "sticker") def __init__( self, sticker: FileInput, emoji_list: Sequence[str], + format: str, # pylint: disable=redefined-builtin mask_position: Optional[MaskPosition] = None, keywords: Optional[Sequence[str]] = None, *, @@ -91,6 +110,7 @@ def __init__( attach=True, ) self.emoji_list: Tuple[str, ...] = parse_sequence_arg(emoji_list) + self.format: str = format self.mask_position: Optional[MaskPosition] = mask_position self.keywords: Tuple[str, ...] = parse_sequence_arg(keywords) diff --git a/telegram/_files/sticker.py b/telegram/_files/sticker.py index cb7b5deac0b..a4d1fb994df 100644 --- a/telegram/_files/sticker.py +++ b/telegram/_files/sticker.py @@ -27,6 +27,8 @@ from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict +from telegram._utils.warnings import warn +from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Bot @@ -227,6 +229,11 @@ class StickerSet(TelegramObject): .. versionchanged:: 20.0 The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` instead. + + .. versionchanged:: 21.1 + 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:: 21.1 + 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:: 21.1 + 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:: 21.1 + 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:: 21.1 + Bot API 7.2 deprecated this field. This parameter will be removed in a future + version of the library. stickers (Tuple[:class:`telegram.Sticker`]): List of all set stickers. .. versionchanged:: 20.0 @@ -289,10 +310,10 @@ def __init__( self, name: str, title: str, - is_animated: bool, stickers: Sequence[Sticker], - is_video: bool, sticker_type: str, + is_animated: Optional[bool] = None, + is_video: Optional[bool] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, @@ -300,13 +321,19 @@ def __init__( super().__init__(api_kwargs=api_kwargs) self.name: str = name self.title: str = title - self.is_animated: bool = is_animated - self.is_video: bool = is_video self.stickers: Tuple[Sticker, ...] = parse_sequence_arg(stickers) self.sticker_type: str = sticker_type # Optional - self.thumbnail: Optional[PhotoSize] = thumbnail + if is_animated is not None or is_video is not None: + warn( + "The parameters `is_animated` and `is_video` are deprecated and will be removed " + "in a future version.", + PTBDeprecationWarning, + stacklevel=2, + ) + self.is_animated: Optional[bool] = is_animated + self.is_video: Optional[bool] = is_video self._id_attrs = (self.name,) self._freeze() diff --git a/telegram/_keyboardbuttonrequest.py b/telegram/_keyboardbuttonrequest.py index 78bb2e50545..fa94433ebd9 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:: 21.1 + request_username (:obj:`bool`, optional): Pass :obj:`True` to request the users' username. + + .. versionadded:: 21.1 + request_photo (:obj:`bool`, optional): Pass :obj:`True` to request the users' photo. + + .. versionadded:: 21.1 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:: 21.1 + request_username (:obj:`bool`): Optional. Pass :obj:`True` to request the users' username. + + .. versionadded:: 21.1 + request_photo (:obj:`bool`): Optional. Pass :obj:`True` to request the users' photo. + + .. versionadded:: 21.1 + """ __slots__ = ( "max_quantity", "request_id", + "request_name", + "request_photo", + "request_username", "user_is_bot", "user_is_premium", ) @@ -86,6 +111,9 @@ def __init__( user_is_bot: Optional[bool] = None, user_is_premium: Optional[bool] = None, max_quantity: Optional[int] = None, + request_name: Optional[bool] = None, + request_username: Optional[bool] = None, + request_photo: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -97,6 +125,9 @@ def __init__( self.user_is_bot: Optional[bool] = user_is_bot self.user_is_premium: Optional[bool] = user_is_premium self.max_quantity: Optional[int] = max_quantity + self.request_name: Optional[bool] = request_name + self.request_username: Optional[bool] = request_username + self.request_photo: Optional[bool] = request_photo self._id_attrs = (self.request_id,) @@ -138,6 +169,15 @@ class KeyboardButtonRequestChat(TelegramObject): applied. bot_is_member (:obj:`bool`, optional): Pass :obj:`True` to request a chat with the bot as a member. Otherwise, no additional restrictions are applied. + request_title (:obj:`bool`, optional): Pass :obj:`True` to request the chat's title. + + .. versionadded:: 21.1 + request_username (:obj:`bool`, optional): Pass :obj:`True` to request the chat's username. + + .. versionadded:: 21.1 + request_photo (:obj:`bool`, optional): Pass :obj:`True` to request the chat's photo. + + .. versionadded:: 21.1 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:: 21.1 + request_username (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's username. + + .. versionadded:: 21.1 + request_photo (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's photo. + + .. versionadded:: 21.1 """ __slots__ = ( @@ -169,6 +218,9 @@ class KeyboardButtonRequestChat(TelegramObject): "chat_is_created", "chat_is_forum", "request_id", + "request_photo", + "request_title", + "request_username", "user_administrator_rights", ) @@ -182,6 +234,9 @@ def __init__( user_administrator_rights: Optional[ChatAdministratorRights] = None, bot_administrator_rights: Optional[ChatAdministratorRights] = None, bot_is_member: Optional[bool] = None, + request_title: Optional[bool] = None, + request_username: Optional[bool] = None, + request_photo: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -199,6 +254,9 @@ def __init__( ) self.bot_administrator_rights: Optional[ChatAdministratorRights] = bot_administrator_rights self.bot_is_member: Optional[bool] = bot_is_member + self.request_title: Optional[bool] = request_title + self.request_username: Optional[bool] = request_username + self.request_photo: Optional[bool] = request_photo self._id_attrs = (self.request_id,) diff --git a/telegram/_message.py b/telegram/_message.py index eac0c748486..502b193a8d5 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:: 21.1 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:: 21.1 + + 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:: 21.1 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:: 21.1 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:: 21.1 + + 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:: 21.1 + .. |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. @@ -824,6 +860,9 @@ class Message(MaybeInaccessibleMessage): .. |blockquote_no_md1_support| replace:: Since block quotation entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a :exc:`ValueError` when encountering a block quotation. + + .. |reply_same_thread| replace:: If :paramref:`message_thread_id` is not provided, + this will reply to the same thread (topic) of the original message. """ # fmt: on @@ -833,6 +872,7 @@ class Message(MaybeInaccessibleMessage): "audio", "author_signature", "boost_added", + "business_connection_id", "caption", "caption_entities", "channel_chat_created", @@ -863,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", @@ -885,6 +926,7 @@ class Message(MaybeInaccessibleMessage): "reply_to_message", "reply_to_story", "sender_boost_count", + "sender_business_bot", "sender_chat", "sticker", "story", @@ -984,6 +1026,9 @@ def __init__( reply_to_story: Optional[Story] = None, boost_added: Optional[ChatBoostAdded] = None, sender_boost_count: Optional[int] = None, + business_connection_id: Optional[str] = None, + sender_business_bot: Optional[User] = None, + is_from_offline: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1079,6 +1124,9 @@ def __init__( self.reply_to_story: Optional[Story] = reply_to_story self.boost_added: Optional[ChatBoostAdded] = boost_added self.sender_boost_count: Optional[int] = sender_boost_count + self.business_connection_id: Optional[str] = business_connection_id + self.sender_business_bot: Optional[User] = sender_business_bot + self.is_from_offline: Optional[bool] = is_from_offline self._effective_attachment = DEFAULT_NONE @@ -1221,6 +1269,7 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Message"]: data["forward_origin"] = MessageOrigin.de_json(data.get("forward_origin"), bot) data["reply_to_story"] = Story.de_json(data.get("reply_to_story"), bot) data["boost_added"] = ChatBoostAdded.de_json(data.get("boost_added"), bot) + data["sender_business_bot"] = User.de_json(data.get("sender_business_bot"), bot) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility @@ -1535,6 +1584,15 @@ async def _parse_quote_arguments( return chat_id, effective_reply_parameters + def _parse_message_thread_id( + self, + chat_id: Union[str, int], + message_thread_id: Optional[int] = None, + ) -> Optional[int]: + return message_thread_id or ( + self.message_thread_id if chat_id in {self.chat_id, self.chat.username} else None + ) + async def reply_text( self, text: str, @@ -1560,10 +1618,19 @@ async def reply_text( ) -> "Message": """Shortcut for:: - await bot.send_message(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -1581,6 +1648,7 @@ async def reply_text( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_message( chat_id=chat_id, text=text, @@ -1599,6 +1667,7 @@ async def reply_text( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_markdown( @@ -1627,7 +1696,9 @@ async def reply_markdown( await bot.send_message( 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, ) @@ -1636,6 +1707,9 @@ async def reply_markdown( For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + .. versionchanged:: 21.1 + |reply_same_thread| + Note: :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by Telegram for backward compatibility. You should use :meth:`reply_markdown_v2` instead. @@ -1656,6 +1730,7 @@ async def reply_markdown( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_message( chat_id=chat_id, text=text, @@ -1674,6 +1749,7 @@ async def reply_markdown( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_markdown_v2( @@ -1702,7 +1778,9 @@ async def reply_markdown_v2( await bot.send_message( 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, ) @@ -1711,6 +1789,9 @@ async def reply_markdown_v2( For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -1727,6 +1808,7 @@ async def reply_markdown_v2( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_message( chat_id=chat_id, text=text, @@ -1745,6 +1827,7 @@ async def reply_markdown_v2( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_html( @@ -1773,7 +1856,9 @@ async def reply_html( await bot.send_message( 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, ) @@ -1782,6 +1867,9 @@ async def reply_html( For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -1798,6 +1886,7 @@ async def reply_html( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_message( chat_id=chat_id, text=text, @@ -1816,6 +1905,7 @@ async def reply_html( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_media_group( @@ -1843,10 +1933,19 @@ async def reply_media_group( ) -> Tuple["Message", ...]: """Shortcut for:: - await bot.send_media_group(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -1866,6 +1965,7 @@ async def reply_media_group( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_media_group( chat_id=chat_id, media=media, @@ -1882,6 +1982,7 @@ async def reply_media_group( caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + business_connection_id=self.business_connection_id, ) async def reply_photo( @@ -1910,10 +2011,19 @@ async def reply_photo( ) -> "Message": """Shortcut for:: - await bot.send_photo(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -1931,6 +2041,7 @@ async def reply_photo( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_photo( chat_id=chat_id, photo=photo, @@ -1950,6 +2061,7 @@ async def reply_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, has_spoiler=has_spoiler, + business_connection_id=self.business_connection_id, ) async def reply_audio( @@ -1981,10 +2093,19 @@ async def reply_audio( ) -> "Message": """Shortcut for:: - await bot.send_audio(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2002,6 +2123,7 @@ async def reply_audio( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_audio( chat_id=chat_id, audio=audio, @@ -2024,6 +2146,7 @@ async def reply_audio( pool_timeout=pool_timeout, api_kwargs=api_kwargs, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_document( @@ -2053,10 +2176,19 @@ async def reply_document( ) -> "Message": """Shortcut for:: - await bot.send_document(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2074,6 +2206,7 @@ async def reply_document( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_document( chat_id=chat_id, document=document, @@ -2094,6 +2227,7 @@ async def reply_document( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_animation( @@ -2126,10 +2260,19 @@ async def reply_animation( ) -> "Message": """Shortcut for:: - await bot.send_animation(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2147,6 +2290,7 @@ async def reply_animation( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_animation( chat_id=chat_id, animation=animation, @@ -2170,6 +2314,7 @@ async def reply_animation( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_sticker( @@ -2194,10 +2339,19 @@ async def reply_sticker( ) -> "Message": """Shortcut for:: - await bot.send_sticker(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2215,6 +2369,7 @@ async def reply_sticker( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_sticker( chat_id=chat_id, sticker=sticker, @@ -2230,6 +2385,7 @@ async def reply_sticker( protect_content=protect_content, message_thread_id=message_thread_id, emoji=emoji, + business_connection_id=self.business_connection_id, ) async def reply_video( @@ -2263,10 +2419,19 @@ async def reply_video( ) -> "Message": """Shortcut for:: - await bot.send_video(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2284,6 +2449,7 @@ async def reply_video( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_video( chat_id=chat_id, video=video, @@ -2308,6 +2474,7 @@ async def reply_video( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_video_note( @@ -2335,10 +2502,19 @@ async def reply_video_note( ) -> "Message": """Shortcut for:: - await bot.send_video_note(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2356,6 +2532,7 @@ async def reply_video_note( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_video_note( chat_id=chat_id, video_note=video_note, @@ -2374,6 +2551,7 @@ async def reply_video_note( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, + business_connection_id=self.business_connection_id, ) async def reply_voice( @@ -2402,10 +2580,19 @@ async def reply_voice( ) -> "Message": """Shortcut for:: - await bot.send_voice(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2423,6 +2610,7 @@ async def reply_voice( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_voice( chat_id=chat_id, voice=voice, @@ -2442,6 +2630,7 @@ async def reply_voice( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, ) async def reply_location( @@ -2471,10 +2660,19 @@ async def reply_location( ) -> "Message": """Shortcut for:: - await bot.send_location(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2492,6 +2690,7 @@ async def reply_location( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_location( chat_id=chat_id, latitude=latitude, @@ -2512,6 +2711,7 @@ async def reply_location( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, ) async def reply_venue( @@ -2543,10 +2743,19 @@ async def reply_venue( ) -> "Message": """Shortcut for:: - await bot.send_venue(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2564,6 +2773,7 @@ async def reply_venue( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_venue( chat_id=chat_id, latitude=latitude, @@ -2586,6 +2796,7 @@ async def reply_venue( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, ) async def reply_contact( @@ -2613,10 +2824,19 @@ async def reply_contact( ) -> "Message": """Shortcut for:: - await bot.send_contact(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2634,6 +2854,7 @@ async def reply_contact( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_contact( chat_id=chat_id, phone_number=phone_number, @@ -2652,6 +2873,7 @@ async def reply_contact( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, ) async def reply_poll( @@ -2686,10 +2908,19 @@ async def reply_poll( ) -> "Message": """Shortcut for:: - await bot.send_poll(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2707,6 +2938,7 @@ async def reply_poll( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_poll( chat_id=chat_id, question=question, @@ -2732,6 +2964,7 @@ async def reply_poll( explanation_entities=explanation_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, ) async def reply_dice( @@ -2755,10 +2988,19 @@ async def reply_dice( ) -> "Message": """Shortcut for:: - await bot.send_dice(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2776,6 +3018,7 @@ async def reply_dice( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_dice( chat_id=chat_id, disable_notification=disable_notification, @@ -2790,6 +3033,7 @@ async def reply_dice( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, ) async def reply_chat_action( @@ -2805,10 +3049,19 @@ async def reply_chat_action( ) -> bool: """Shortcut for:: - await bot.send_chat_action(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`. + .. versionchanged:: 21.1 + |reply_same_thread| + .. versionadded:: 13.2 Returns: @@ -2817,13 +3070,14 @@ async def reply_chat_action( """ return await self.get_bot().send_chat_action( chat_id=self.chat_id, - message_thread_id=message_thread_id, + message_thread_id=self._parse_message_thread_id(self.chat_id, message_thread_id), action=action, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def reply_game( @@ -2847,10 +3101,19 @@ async def reply_game( ) -> "Message": """Shortcut for:: - await bot.send_game(update.effective_message.chat_id, *args, **kwargs) + 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, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -2870,8 +3133,9 @@ async def reply_game( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_game( - chat_id=chat_id, + chat_id=chat_id, # type: ignore[arg-type] game_short_name=game_short_name, disable_notification=disable_notification, reply_parameters=effective_reply_parameters, @@ -2884,6 +3148,7 @@ async def reply_game( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, ) async def reply_invoice( @@ -2927,10 +3192,18 @@ async def reply_invoice( ) -> "Message": """Shortcut for:: - await bot.send_invoice(update.effective_message.chat_id, *args, **kwargs) + await bot.send_invoice( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + *args, + **kwargs, + ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`. + .. versionchanged:: 21.1 + |reply_same_thread| + Warning: As of API 5.2 :paramref:`start_parameter ` is an optional argument and therefore the @@ -2960,6 +3233,7 @@ async def reply_invoice( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_invoice( chat_id=chat_id, title=title, @@ -3130,6 +3404,7 @@ async def reply_copy( await bot.copy_message( chat_id=message.chat.id, + message_thread_id=update.effective_message.message_thread_id, message_id=message_id, *args, **kwargs @@ -3137,6 +3412,9 @@ async def reply_copy( For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + .. versionchanged:: 21.1 + |reply_same_thread| + Keyword Args: quote (:obj:`bool`, optional): |reply_quote| @@ -3155,6 +3433,7 @@ async def reply_copy( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, quote, reply_to_message_id, reply_parameters ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().copy_message( chat_id=chat_id, from_chat_id=from_chat_id, diff --git a/telegram/_passport/encryptedpassportelement.py b/telegram/_passport/encryptedpassportelement.py index 14d8b63dcf5..e6a22ee2e7e 100644 --- a/telegram/_passport/encryptedpassportelement.py +++ b/telegram/_passport/encryptedpassportelement.py @@ -60,8 +60,8 @@ class EncryptedPassportElement(TelegramObject): email (:obj:`str`, optional): User's verified email address; available only for "email" type. files (Sequence[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted - files with documents provided by the user; available only for "utility_bill", - "bank_statement", "rental_agreement", "passport_registration" and + files with documents provided by the user; available only for "utility_bill", + "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 @@ -74,12 +74,12 @@ class EncryptedPassportElement(TelegramObject): reverse side of the document, provided by the user; Available only for "driver_license" and "identity_card". selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the - selfie of the user holding a document, provided by the user; available if requested for + selfie of the user holding a document, provided by the user; available if requested for "passport", "driver_license", "identity_card" and "internal_passport". translation (Sequence[:class:`telegram.PassportFile`], optional): Array of - encrypted/decrypted files with translated versions of documents provided by the user; - available if requested requested for "passport", "driver_license", "identity_card", - "internal_passport", "utility_bill", "bank_statement", "rental_agreement", + encrypted/decrypted files with translated versions of documents provided by the user; + available if requested requested for "passport", "driver_license", "identity_card", + "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 @@ -101,8 +101,8 @@ class EncryptedPassportElement(TelegramObject): email (:obj:`str`): Optional. User's verified email address; available only for "email" type. files (Tuple[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted - files with documents provided by the user; available only for "utility_bill", - "bank_statement", "rental_agreement", "passport_registration" and + files with documents provided by the user; available only for "utility_bill", + "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 diff --git a/telegram/_passport/passportfile.py b/telegram/_passport/passportfile.py index 3d37910629c..12c0f6f049d 100644 --- a/telegram/_passport/passportfile.py +++ b/telegram/_passport/passportfile.py @@ -203,5 +203,6 @@ async def get_file( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - file.set_credentials(self._credentials) + if self._credentials: + file.set_credentials(self._credentials) return file diff --git a/telegram/_shared.py b/telegram/_shared.py index 89cb0b5d6a2..70180044703 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:: 21.1 + 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:: 21.1 + + .. deprecated:: 21.1 + 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:: 21.1 + 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:: 21.1 """ - __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:: 21.1 + 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:: 21.1 + username (:obj:`str`, optional): Username of the chat, if the username was requested by + the bot and available. + + .. versionadded:: 21.1 + photo (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the chat photo, + if the photo was requested by the bot + + .. versionadded:: 21.1 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:: 21.1 + username (:obj:`str`): Optional. Username of the chat, if the username was requested by + the bot and available. + + .. versionadded:: 21.1 + photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Available sizes of the chat photo, + if the photo was requested by the bot + + .. versionadded:: 21.1 """ - __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:: 21.1 + + 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 566ca9cfd3f..ada70da258c 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -18,9 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Update.""" -from typing import TYPE_CHECKING, Final, List, Optional +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:: 21.1 + + business_message (:class:`telegram.Message`, optional): New non-service message + from a connected business account. + + .. versionadded:: 21.1 + + edited_business_message (:class:`telegram.Message`, optional): New version of a message + from a connected business account. + + .. versionadded:: 21.1 + + deleted_business_messages (:class:`telegram.BusinessMessagesDeleted`, optional): Messages + were deleted from a connected business account. + + .. versionadded:: 21.1 + + 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,18 +242,44 @@ 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:: 21.1 + + business_message (:class:`telegram.Message`): Optional. New non-service message + from a connected business account. + + .. versionadded:: 21.1 + + edited_business_message (:class:`telegram.Message`): Optional. New version of a message + from a connected business account. + + .. versionadded:: 21.1 + + deleted_business_messages (:class:`telegram.BusinessMessagesDeleted`): Optional. Messages + were deleted from a connected business account. + + .. versionadded:: 21.1 """ __slots__ = ( "_effective_chat", "_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", @@ -318,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:: 21.1""" + BUSINESS_MESSAGE: Final[str] = constants.UpdateType.BUSINESS_MESSAGE + """:const:`telegram.constants.UpdateType.BUSINESS_MESSAGE` + + .. versionadded:: 21.1""" + EDITED_BUSINESS_MESSAGE: Final[str] = constants.UpdateType.EDITED_BUSINESS_MESSAGE + """:const:`telegram.constants.UpdateType.EDITED_BUSINESS_MESSAGE` + + .. versionadded:: 21.1""" + DELETED_BUSINESS_MESSAGES: Final[str] = constants.UpdateType.DELETED_BUSINESS_MESSAGES + """:const:`telegram.constants.UpdateType.DELETED_BUSINESS_MESSAGES` + + .. versionadded:: 21.1""" ALL_TYPES: Final[List[str]] = list(constants.UpdateType) """List[:obj:`str`]: A list of all available update types. @@ -344,6 +409,10 @@ def __init__( removed_chat_boost: Optional[ChatBoostRemoved] = None, message_reaction: Optional[MessageReactionUpdated] = None, message_reaction_count: Optional[MessageReactionCountUpdated] = None, + business_connection: Optional[BusinessConnection] = None, + business_message: Optional[Message] = None, + edited_business_message: Optional[Message] = None, + deleted_business_messages: Optional[BusinessMessagesDeleted] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -369,8 +438,15 @@ def __init__( self.removed_chat_boost: Optional[ChatBoostRemoved] = removed_chat_boost self.message_reaction: Optional[MessageReactionUpdated] = message_reaction self.message_reaction_count: Optional[MessageReactionCountUpdated] = message_reaction_count + self.business_connection: Optional[BusinessConnection] = business_connection + self.business_message: Optional[Message] = business_message + self.edited_business_message: Optional[Message] = edited_business_message + self.deleted_business_messages: Optional[BusinessMessagesDeleted] = ( + deleted_business_messages + ) self._effective_user: Optional[User] = None + self._effective_sender: Optional[Union["User", "Chat"]] = None self._effective_chat: Optional[Chat] = None self._effective_message: Optional[Message] = None @@ -391,9 +467,14 @@ def effective_user(self) -> Optional["User"]: * :attr:`chat_boost` * :attr:`removed_chat_boost` * :attr:`message_reaction_count` + * :attr:`deleted_business_messages` is present. + .. versionchanged:: 21.1 + 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`. @@ -441,9 +522,76 @@ def effective_user(self) -> Optional["User"]: elif self.message_reaction: user = self.message_reaction.user + elif self.business_message: + user = self.business_message.from_user + + elif self.edited_business_message: + user = self.edited_business_message.from_user + + elif self.business_connection: + user = self.business_connection.user + self._effective_user = user return user + @property + def effective_sender(self) -> Optional[Union["User", "Chat"]]: + """ + :class:`telegram.User` or :class:`telegram.Chat`: The user or chat that sent this update, + no matter what kind of update this is. + + Note: + * Depending on the type of update and the user's 'Remain anonymous' setting, this + could either be :class:`telegram.User`, :class:`telegram.Chat` or :obj:`None`. + + If no user whatsoever is associated with this update, this gives :obj:`None`. This + is the case if any of + + * :attr:`poll` + * :attr:`chat_boost` + * :attr:`removed_chat_boost` + * :attr:`message_reaction_count` + * :attr:`deleted_business_messages` + + is present. + + Example: + * If :attr:`message` is present, this will give either + :attr:`telegram.Message.from_user` or :attr:`telegram.Message.sender_chat`. + * If :attr:`poll_answer` is present, this will give either + :attr:`telegram.PollAnswer.user` or :attr:`telegram.PollAnswer.voter_chat`. + * If :attr:`channel_post` is present, this will give + :attr:`telegram.Message.sender_chat`. + + .. versionadded:: 21.1 + """ + if self._effective_sender: + return self._effective_sender + + sender: Optional[Union["User", "Chat"]] = None + + if message := ( + 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 + + elif self.poll_answer: + sender = self.poll_answer.voter_chat + + elif self.message_reaction: + sender = self.message_reaction.actor_chat + + if sender is None: + sender = self.effective_user + + self._effective_sender = sender + return sender + @property def effective_chat(self) -> Optional["Chat"]: """ @@ -452,8 +600,12 @@ def effective_chat(self) -> Optional["Chat"]: If no chat is associated with this update, this gives :obj:`None`. This is the case, if :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, - :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll` or - :attr:`poll_answer` is present. + :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, + :attr:`poll_answer`, or :attr:`business_connection` is present. + + .. versionchanged:: 21.1 + 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`. @@ -500,6 +652,15 @@ def effective_chat(self) -> Optional["Chat"]: elif self.message_reaction_count: chat = self.message_reaction_count.chat + elif self.business_message: + chat = self.business_message.chat + + elif self.edited_business_message: + chat = self.edited_business_message.chat + + elif self.deleted_business_messages: + chat = self.deleted_business_messages.chat + self._effective_chat = chat return chat @@ -512,6 +673,10 @@ def effective_message(self) -> Optional[Message]: :attr:`callback_query` (i.e. :attr:`telegram.CallbackQuery.message`) or :obj:`None`, if none of those are present. + .. versionchanged:: 21.1 + 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 @@ -554,6 +719,12 @@ def effective_message(self) -> Optional[Message]: elif self.edited_channel_post: message = self.edited_channel_post + elif self.business_message: + message = self.business_message + + elif self.edited_business_message: + message = self.edited_business_message + self._effective_message = message return message @@ -589,5 +760,13 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Update"]: data["message_reaction_count"] = MessageReactionCountUpdated.de_json( data.get("message_reaction_count"), bot ) + data["business_connection"] = BusinessConnection.de_json( + data.get("business_connection"), bot + ) + data["business_message"] = Message.de_json(data.get("business_message"), bot) + data["edited_business_message"] = Message.de_json(data.get("edited_business_message"), bot) + data["deleted_business_messages"] = BusinessMessagesDeleted.de_json( + data.get("deleted_business_messages"), bot + ) return super().de_json(data=data, bot=bot) diff --git a/telegram/_user.py b/telegram/_user.py index eb4227e18ce..ef6c4f4f504 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:: 21.1 + 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:: 21.1 .. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id` coincides with the :attr:`Chat.id` of the private chat with the user. This has been the case so far, but Telegram does not guarantee that this stays this way. @@ -119,6 +130,7 @@ class User(TelegramObject): __slots__ = ( "added_to_attachment_menu", + "can_connect_to_business", "can_join_groups", "can_read_all_group_messages", "first_name", @@ -144,6 +156,7 @@ def __init__( supports_inline_queries: Optional[bool] = None, is_premium: Optional[bool] = None, added_to_attachment_menu: Optional[bool] = None, + can_connect_to_business: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -161,6 +174,7 @@ def __init__( self.supports_inline_queries: Optional[bool] = supports_inline_queries self.is_premium: Optional[bool] = is_premium self.added_to_attachment_menu: Optional[bool] = added_to_attachment_menu + self.can_connect_to_business: Optional[bool] = can_connect_to_business self._id_attrs = (self.id,) @@ -393,6 +407,7 @@ async def send_message( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, disable_web_page_preview: Optional[bool] = None, @@ -435,6 +450,7 @@ async def send_message( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def delete_message( @@ -513,6 +529,7 @@ async def send_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -556,6 +573,7 @@ async def send_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, has_spoiler=has_spoiler, + business_connection_id=business_connection_id, ) async def send_media_group( @@ -567,6 +585,7 @@ async def send_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -610,6 +629,7 @@ async def send_media_group( caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + business_connection_id=business_connection_id, ) async def send_audio( @@ -627,6 +647,7 @@ async def send_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -673,12 +694,14 @@ async def send_audio( pool_timeout=pool_timeout, api_kwargs=api_kwargs, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_chat_action( self, action: str, message_thread_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -708,6 +731,7 @@ async def send_chat_action( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) send_action = send_chat_action @@ -724,6 +748,7 @@ async def send_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -766,6 +791,7 @@ async def send_contact( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_dice( @@ -776,6 +802,7 @@ async def send_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -813,6 +840,7 @@ async def send_dice( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_document( @@ -828,6 +856,7 @@ async def send_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -872,6 +901,7 @@ async def send_document( caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_game( @@ -882,6 +912,7 @@ async def send_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -919,6 +950,7 @@ async def send_game( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_invoice( @@ -1031,6 +1063,7 @@ async def send_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1075,6 +1108,7 @@ async def send_location( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_animation( @@ -1093,6 +1127,7 @@ async def send_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1140,6 +1175,7 @@ async def send_animation( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_sticker( @@ -1151,6 +1187,7 @@ async def send_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1189,6 +1226,7 @@ async def send_sticker( protect_content=protect_content, message_thread_id=message_thread_id, emoji=emoji, + business_connection_id=business_connection_id, ) async def send_video( @@ -1208,6 +1246,7 @@ async def send_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1256,6 +1295,7 @@ async def send_video( protect_content=protect_content, message_thread_id=message_thread_id, has_spoiler=has_spoiler, + business_connection_id=business_connection_id, ) async def send_venue( @@ -1273,6 +1313,7 @@ async def send_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1319,6 +1360,7 @@ async def send_venue( allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_video_note( @@ -1332,6 +1374,7 @@ async def send_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1374,6 +1417,7 @@ async def send_video_note( protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, + business_connection_id=business_connection_id, ) async def send_voice( @@ -1388,6 +1432,7 @@ async def send_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1431,6 +1476,7 @@ async def send_voice( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_poll( @@ -1452,6 +1498,7 @@ async def send_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1500,6 +1547,7 @@ async def send_poll( explanation_entities=explanation_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, ) async def send_copy( diff --git a/telegram/_version.py b/telegram/_version.py index 1469f71bb73..fd81c535668 100644 --- a/telegram/_version.py +++ b/telegram/_version.py @@ -51,7 +51,7 @@ def __str__(self) -> str: __version_info__: Final[Version] = Version( - major=21, minor=0, micro=1, releaselevel="final", serial=0 + major=21, minor=1, micro=0, releaselevel="final", serial=0 ) __version__: Final[str] = str(__version_info__) diff --git a/telegram/constants.py b/telegram/constants.py index 959e99ac454..2eac123fc14 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__`. @@ -1690,8 +1690,12 @@ class MessageOriginType(StringEnum): class MessageType(StringEnum): - """This enum contains the available types of :class:`telegram.Message`. The enum - members of this enumeration are instances of :class:`str` and can be treated as such. + """This enum contains the available types of :class:`telegram.Message`. Here, a "type" means + a kind of message that is visually distinct from other kinds of messages in the Telegram app. + In particular, auxiliary attributes that can be present for multiple types of messages are + not considered in this enumeration. + + The enum members of this enumeration are instances of :class:`str` and can be treated as such. .. versionadded:: 20.0 """ @@ -1710,6 +1714,11 @@ class MessageType(StringEnum): .. versionadded:: 21.0 """ + BUSINESS_CONNECTION_ID = "business_connection_id" + """:obj:`str`: Messages with :attr:`telegram.Message.business_connection_id`. + + .. versionadded:: 21.1 + """ CHANNEL_CHAT_CREATED = "channel_chat_created" """:obj:`str`: Messages with :attr:`telegram.Message.channel_chat_created`.""" CHAT_SHARED = "chat_shared" @@ -1817,6 +1826,11 @@ class MessageType(StringEnum): .. versionadded:: 21.0 """ + SENDER_BUSINESS_BOT = "sender_business_bot" + """:obj:`str`: Messages with :attr:`telegram.Message.sender_business_bot`. + + .. versionadded:: 21.1 + """ STICKER = "sticker" """:obj:`str`: Messages with :attr:`telegram.Message.sticker`.""" STORY = "story" @@ -2312,6 +2326,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:: 21.1 + 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 +2521,26 @@ class UpdateType(StringEnum): .. versionadded:: 20.8 """ + BUSINESS_CONNECTION = "business_connection" + """:obj:`str`: Updates with :attr:`telegram.Update.business_connection`. + + .. versionadded:: 21.1 + """ + BUSINESS_MESSAGE = "business_message" + """:obj:`str`: Updates with :attr:`telegram.Update.business_message`. + + .. versionadded:: 21.1 + """ + EDITED_BUSINESS_MESSAGE = "edited_business_message" + """:obj:`str`: Updates with :attr:`telegram.Update.edited_business_message`. + + .. versionadded:: 21.1 + """ + DELETED_BUSINESS_MESSAGES = "deleted_business_messages" + """:obj:`str`: Updates with :attr:`telegram.Update.deleted_business_messages`. + + .. versionadded:: 21.1 + """ class InvoiceLimit(IntEnum): diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index d1101bcf21c..82dbd1c19ad 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -27,6 +27,8 @@ "BasePersistence", "BaseRateLimiter", "BaseUpdateProcessor", + "BusinessConnectionHandler", + "BusinessMessagesDeletedHandler", "CallbackContext", "CallbackDataCache", "CallbackQueryHandler", @@ -75,6 +77,8 @@ from ._dictpersistence import DictPersistence from ._extbot import ExtBot from ._handlers.basehandler import BaseHandler +from ._handlers.businessconnectionhandler import BusinessConnectionHandler +from ._handlers.businessmessagesdeletedhandler import BusinessMessagesDeletedHandler from ._handlers.callbackqueryhandler import CallbackQueryHandler from ._handlers.chatboosthandler import ChatBoostHandler from ._handlers.chatjoinrequesthandler import ChatJoinRequestHandler diff --git a/telegram/ext/_application.py b/telegram/ext/_application.py index 4f951741dc0..8c32910a5fa 100644 --- a/telegram/ext/_application.py +++ b/telegram/ext/_application.py @@ -75,6 +75,8 @@ from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: + from socket import socket + from telegram import Message from telegram.ext import ConversationHandler, JobQueue from telegram.ext._applicationbuilder import InitApplicationBuilder @@ -866,7 +868,7 @@ def run_webhook( close_loop: bool = True, stop_signals: ODVInput[Sequence[int]] = DEFAULT_NONE, secret_token: Optional[str] = None, - unix: Optional[Union[str, Path]] = None, + unix: Optional[Union[str, Path, "socket"]] = None, ) -> None: """Convenience method that takes care of initializing and starting the app, listening for updates from Telegram using :meth:`telegram.ext.Updater.start_webhook` and @@ -959,8 +961,17 @@ def run_webhook( header isn't set or it is set to a wrong token. .. versionadded:: 20.0 - unix (:class:`pathlib.Path` | :obj:`str`, optional): Path to the unix socket file. Path - does not need to exist, in which case the file will be created. + unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be + either: + + * the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This + will be passed to `tornado.netutil.bind_unix_socket `_ to create the socket. + If the Path does not exist, the file will be created. + + * or the socket itself. This option allows you to e.g. restrict the permissions of + the socket for improved security. Note that you need to pass the correct family, + type and socket options yourself. Caution: This parameter is a replacement for the default TCP bind. Therefore, it is @@ -969,6 +980,8 @@ def run_webhook( appropriate :paramref:`webhook_url`. .. versionadded:: 20.8 + .. versionchanged:: 21.1 + Added support to pass a socket instance itself. """ if not self.updater: raise RuntimeError( diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index eefda3bf3d7..7b5649ebea3 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -48,6 +48,7 @@ BotDescription, BotName, BotShortDescription, + BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -113,6 +114,7 @@ from telegram.ext import BaseRateLimiter, Defaults HandledTypes = TypeVar("HandledTypes", bound=Union[Message, CallbackQuery, Chat]) +KT = TypeVar("KT", bound=ReplyMarkup) class ExtBot(Bot, Generic[RLARGS]): @@ -485,11 +487,14 @@ def _insert_defaults(self, data: Dict[str, object]) -> None: data[key] = new_value - def _replace_keyboard(self, reply_markup: Optional[ReplyMarkup]) -> Optional[ReplyMarkup]: + def _replace_keyboard(self, reply_markup: Optional[KT]) -> Optional[KT]: # If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the # CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input if isinstance(reply_markup, InlineKeyboardMarkup) and self.callback_data_cache is not None: - return self.callback_data_cache.process_keyboard(reply_markup) + # for some reason mypy doesn't understand that IKB is a subtype of Optional[KT] + return self.callback_data_cache.process_keyboard( # type: ignore[return-value] + reply_markup + ) return reply_markup @@ -567,6 +572,7 @@ async def _send_message( caption_entities: Optional[Sequence["MessageEntity"]] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -597,6 +603,7 @@ async def _send_message( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) if isinstance(result, Message): self._insert_callback_data(result) @@ -1178,7 +1185,7 @@ async def create_new_sticker_set( name: str, title: str, stickers: Sequence["InputSticker"], - sticker_format: str, + sticker_format: Optional[str] = None, sticker_type: Optional[str] = None, needs_repainting: Optional[bool] = None, *, @@ -2351,6 +2358,7 @@ async def send_animation( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2384,6 +2392,7 @@ async def send_animation( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, + business_connection_id=business_connection_id, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) @@ -2404,6 +2413,7 @@ async def send_audio( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2420,6 +2430,7 @@ async def send_audio( audio=audio, duration=duration, performer=performer, + business_connection_id=business_connection_id, title=title, caption=caption, disable_notification=disable_notification, @@ -2445,6 +2456,7 @@ async def send_chat_action( chat_id: Union[str, int], action: str, message_thread_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2455,6 +2467,7 @@ async def send_chat_action( ) -> bool: return await super().send_chat_action( chat_id=chat_id, + business_connection_id=business_connection_id, action=action, message_thread_id=message_thread_id, read_timeout=read_timeout, @@ -2476,6 +2489,7 @@ async def send_contact( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2505,6 +2519,7 @@ async def send_contact( write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, + business_connection_id=business_connection_id, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) @@ -2517,6 +2532,7 @@ async def send_dice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2530,6 +2546,7 @@ async def send_dice( return await super().send_dice( chat_id=chat_id, disable_notification=disable_notification, + business_connection_id=business_connection_id, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, emoji=emoji, @@ -2558,6 +2575,7 @@ async def send_document( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2581,6 +2599,7 @@ async def send_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + business_connection_id=business_connection_id, message_thread_id=message_thread_id, thumbnail=thumbnail, reply_parameters=reply_parameters, @@ -2601,6 +2620,7 @@ async def send_game( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2617,6 +2637,7 @@ async def send_game( disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, + business_connection_id=business_connection_id, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, @@ -2718,6 +2739,7 @@ async def send_location( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2748,6 +2770,7 @@ async def send_location( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, + business_connection_id=business_connection_id, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) @@ -2762,6 +2785,7 @@ async def send_media_group( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2790,6 +2814,7 @@ async def send_media_group( pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), caption=caption, + business_connection_id=business_connection_id, parse_mode=parse_mode, caption_entities=caption_entities, ) @@ -2806,6 +2831,7 @@ async def send_message( message_thread_id: Optional[int] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, disable_web_page_preview: Optional[bool] = None, reply_to_message_id: Optional[int] = None, @@ -2824,6 +2850,7 @@ async def send_message( entities=entities, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, + business_connection_id=business_connection_id, protect_content=protect_content, message_thread_id=message_thread_id, reply_to_message_id=reply_to_message_id, @@ -2851,6 +2878,7 @@ async def send_photo( message_thread_id: Optional[int] = None, has_spoiler: Optional[bool] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2877,6 +2905,7 @@ async def send_photo( has_spoiler=has_spoiler, reply_parameters=reply_parameters, filename=filename, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2904,6 +2933,7 @@ async def send_poll( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2932,6 +2962,7 @@ async def send_poll( close_date=close_date, allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, + business_connection_id=business_connection_id, protect_content=protect_content, message_thread_id=message_thread_id, reply_parameters=reply_parameters, @@ -2952,6 +2983,7 @@ async def send_sticker( message_thread_id: Optional[int] = None, emoji: Optional[str] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2968,6 +3000,7 @@ async def send_sticker( disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, + business_connection_id=business_connection_id, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, @@ -2996,6 +3029,7 @@ async def send_venue( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3022,6 +3056,7 @@ async def send_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + business_connection_id=business_connection_id, message_thread_id=message_thread_id, reply_parameters=reply_parameters, venue=venue, @@ -3050,6 +3085,7 @@ async def send_video( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3077,6 +3113,7 @@ async def send_video( caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, + business_connection_id=business_connection_id, has_spoiler=has_spoiler, thumbnail=thumbnail, filename=filename, @@ -3100,6 +3137,7 @@ async def send_video_note( message_thread_id: Optional[int] = None, thumbnail: Optional[FileInput] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3130,6 +3168,7 @@ async def send_video_note( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + business_connection_id=business_connection_id, ) async def send_voice( @@ -3145,6 +3184,7 @@ async def send_voice( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3176,6 +3216,7 @@ async def send_voice( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + business_connection_id=business_connection_id, ) async def set_chat_administrator_custom_title( @@ -3462,6 +3503,7 @@ async def set_sticker_set_thumbnail( self, name: str, user_id: int, + format: str, # pylint: disable=redefined-builtin thumbnail: Optional[FileInput] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3475,6 +3517,7 @@ async def set_sticker_set_thumbnail( name=name, user_id=user_id, thumbnail=thumbnail, + format=format, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -3998,6 +4041,52 @@ async def set_message_reaction( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_business_connection( + self, + business_connection_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> BusinessConnection: + return await super().get_business_connection( + business_connection_id=business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def replace_sticker_in_set( + self, + user_id: int, + name: str, + old_sticker: str, + sticker: "InputSticker", + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> bool: + return await super().replace_sticker_in_set( + user_id=user_id, + name=name, + old_sticker=old_sticker, + sticker=sticker, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + # updated camelCase aliases getMe = get_me sendMessage = send_message @@ -4117,3 +4206,5 @@ async def set_message_reaction( unpinAllGeneralForumTopicMessages = unpin_all_general_forum_topic_messages getUserChatBoosts = get_user_chat_boosts setMessageReaction = set_message_reaction + getBusinessConnection = get_business_connection + replaceStickerInSet = replace_sticker_in_set diff --git a/telegram/ext/_handlers/businessconnectionhandler.py b/telegram/ext/_handlers/businessconnectionhandler.py new file mode 100644 index 00000000000..c8cb3e843d0 --- /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:: 21.1 + + Args: + callback (:term:`coroutine function`): The callback function for this handler. Will be + called when :meth:`check_update` has determined that an update should be processed by + this handler. Callback signature:: + + async def callback(update: Update, context: CallbackContext) + user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only + those which are from the specified user ID(s). + + username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only + those which are from the specified username(s). + + block (:obj:`bool`, optional): Determines whether the return value of the callback should + be awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. + + .. seealso:: :wiki:`Concurrency` + Attributes: + callback (:term:`coroutine function`): The callback function for this handler. + block (:obj:`bool`): Determines whether the return value of the callback should be + awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. + """ + + __slots__ = ( + "_user_ids", + "_usernames", + ) + + def __init__( + self, + callback: HandlerCallback[Update, CCT, RT], + user_id: Optional[SCT[int]] = None, + username: Optional[SCT[str]] = None, + block: DVType[bool] = DEFAULT_TRUE, + ): + super().__init__(callback, block=block) + + self._user_ids = parse_chat_id(user_id) + self._usernames = parse_username(username) + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handler's :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + if isinstance(update, Update) and update.business_connection: + if not self._user_ids and not self._usernames: + return True + if update.business_connection.user.id in self._user_ids: + return True + return update.business_connection.user.username in self._usernames + return False diff --git a/telegram/ext/_handlers/businessmessagesdeletedhandler.py b/telegram/ext/_handlers/businessmessagesdeletedhandler.py new file mode 100644 index 00000000000..0ceb58fe05c --- /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:: 21.1 + + 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/_handlers/chatjoinrequesthandler.py b/telegram/ext/_handlers/chatjoinrequesthandler.py index 9e4f3a011af..7007f61ab03 100644 --- a/telegram/ext/_handlers/chatjoinrequesthandler.py +++ b/telegram/ext/_handlers/chatjoinrequesthandler.py @@ -107,7 +107,5 @@ def check_update(self, update: object) -> bool: return True if update.chat_join_request.chat.id in self._chat_ids: return True - if update.chat_join_request.from_user.username in self._usernames: - return True - return False + return update.chat_join_request.from_user.username in self._usernames return False diff --git a/telegram/ext/_handlers/commandhandler.py b/telegram/ext/_handlers/commandhandler.py index b509918d797..94ee77493bb 100644 --- a/telegram/ext/_handlers/commandhandler.py +++ b/telegram/ext/_handlers/commandhandler.py @@ -153,14 +153,12 @@ def _check_correct_args(self, args: List[str]) -> Optional[bool]: :obj:`bool`: Whether the args are valid for this handler. """ # pylint: disable=too-many-boolean-expressions - if ( + return bool( (self.has_args is None) or (self.has_args is True and args) or (self.has_args is False and not args) or (isinstance(self.has_args, int) and len(args) == self.has_args) - ): - return True - return False + ) def check_update( self, update: object diff --git a/telegram/ext/_updater.py b/telegram/ext/_updater.py index 000176288ae..04d01a83eae 100644 --- a/telegram/ext/_updater.py +++ b/telegram/ext/_updater.py @@ -49,6 +49,8 @@ WEBHOOKS_AVAILABLE = False if TYPE_CHECKING: + from socket import socket + from telegram import Bot @@ -472,7 +474,7 @@ async def start_webhook( ip_address: Optional[str] = None, max_connections: int = 40, secret_token: Optional[str] = None, - unix: Optional[Union[str, Path]] = None, + unix: Optional[Union[str, Path, "socket"]] = None, ) -> "asyncio.Queue[object]": """ Starts a small http server to listen for updates via webhook. If :paramref:`cert` @@ -541,8 +543,17 @@ async def start_webhook( header isn't set or it is set to a wrong token. .. versionadded:: 20.0 - unix (:class:`pathlib.Path` | :obj:`str`, optional): Path to the unix socket file. Path - does not need to exist, in which case the file will be created. + unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be + either: + + * the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This + will be passed to `tornado.netutil.bind_unix_socket `_ to create the socket. + If the Path does not exist, the file will be created. + + * or the socket itself. This option allows you to e.g. restrict the permissions of + the socket for improved security. Note that you need to pass the correct family, + type and socket options yourself. Caution: This parameter is a replacement for the default TCP bind. Therefore, it is @@ -551,6 +562,8 @@ async def start_webhook( appropriate :paramref:`webhook_url`. .. versionadded:: 20.8 + .. versionchanged:: 21.1 + Added support to pass a socket instance itself. Returns: :class:`queue.Queue`: The update queue that can be filled from the main thread. @@ -632,7 +645,7 @@ async def _start_webhook( ip_address: Optional[str] = None, max_connections: int = 40, secret_token: Optional[str] = None, - unix: Optional[Union[str, Path]] = None, + unix: Optional[Union[str, Path, "socket"]] = None, ) -> None: _LOGGER.debug("Updater thread started (webhook)") @@ -793,7 +806,7 @@ async def bootstrap_set_webhook() -> bool: if drop_pending_updates: _LOGGER.debug("Dropping pending updates from Telegram server") await self.bot.set_webhook( - url=webhook_url, + url=webhook_url, # type: ignore[arg-type] certificate=cert, allowed_updates=allowed_updates, ip_address=ip_address, diff --git a/telegram/ext/_utils/webhookhandler.py b/telegram/ext/_utils/webhookhandler.py index f630ec94185..828dbca4715 100644 --- a/telegram/ext/_utils/webhookhandler.py +++ b/telegram/ext/_utils/webhookhandler.py @@ -21,6 +21,7 @@ import json from http import HTTPStatus from pathlib import Path +from socket import socket from ssl import SSLContext from types import TracebackType from typing import TYPE_CHECKING, Optional, Type, Union @@ -67,7 +68,7 @@ def __init__( port: int, webhook_app: "WebhookAppClass", ssl_ctx: Optional[SSLContext], - unix: Optional[Union[str, Path]] = None, + unix: Optional[Union[str, Path, socket]] = None, ): if unix and not UNIX_AVAILABLE: raise RuntimeError("This OS does not support binding unix sockets.") @@ -75,15 +76,18 @@ def __init__( self.listen = listen self.port = port self.is_running = False - self.unix = unix + self.unix = None + if unix and isinstance(unix, socket): + self.unix = unix + elif unix: + self.unix = bind_unix_socket(str(unix)) self._server_lock = asyncio.Lock() self._shutdown_lock = asyncio.Lock() async def serve_forever(self, ready: Optional[asyncio.Event] = None) -> None: async with self._server_lock: if self.unix: - socket = bind_unix_socket(str(self.unix)) - self._http_server.add_socket(socket) + self._http_server.add_socket(self.unix) else: self._http_server.listen(self.port, address=self.listen) diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index aad3ad95dc6..614673628e1 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -54,6 +54,7 @@ "HAS_PROTECTED_CONTENT", "INVOICE", "IS_AUTOMATIC_FORWARD", + "IS_FROM_OFFLINE", "IS_TOPIC_MESSAGE", "LOCATION", "PASSPORT_DATA", @@ -272,23 +273,29 @@ def name(self, name: str) -> None: def check_update(self, update: Update) -> Optional[Union[bool, FilterDataDict]]: """Checks if the specified update should be handled by this filter. + .. versionchanged:: 21.1 + 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. """ - if ( # Only message updates should be handled. - update.channel_post + return bool( # Only message updates should be handled. + update.channel_post # pylint: disable=too-many-boolean-expressions or update.message or update.edited_channel_post or update.edited_message - ): - return True - return False + or update.business_message + or update.edited_business_message + ) class MessageFilter(BaseFilter): @@ -1556,6 +1563,20 @@ def filter(self, message: Message) -> bool: """ +class _IsFromOffline(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.is_from_offline) + + +IS_FROM_OFFLINE = _IsFromOffline(name="filters.IS_FROM_OFFLINE") +"""Messages that contain :attr:`telegram.Message.is_from_offline`. + + .. versionadded:: 21.1 +""" + + class Language(MessageFilter): """Filters messages to only allow those which are from users with a certain language code. @@ -2488,13 +2509,21 @@ class _Edited(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: - return update.edited_message is not None or update.edited_channel_post is not None + return ( + update.edited_message is not None + or update.edited_channel_post is not None + or update.edited_business_message is not None + ) EDITED = _Edited(name="filters.UpdateType.EDITED") - """Updates with either :attr:`telegram.Update.edited_message` or - :attr:`telegram.Update.edited_channel_post`. + """Updates with :attr:`telegram.Update.edited_message`, + :attr:`telegram.Update.edited_channel_post`, or + :attr:`telegram.Update.edited_business_message`. .. versionadded:: 20.0 + + .. versionchanged:: 21.1 + Added :attr:`telegram.Update.edited_business_message` to the filter. """ class _EditedChannelPost(UpdateFilter): @@ -2532,7 +2561,48 @@ def filter(self, update: Update) -> bool: MESSAGES = _Messages(name="filters.UpdateType.MESSAGES") """Updates with either :attr:`telegram.Update.message` or - :attr:`telegram.Update.edited_message`.""" + :attr:`telegram.Update.edited_message`. + """ + + class _BusinessMessage(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return update.business_message is not None + + BUSINESS_MESSAGE = _BusinessMessage(name="filters.UpdateType.BUSINESS_MESSAGE") + """Updates with :attr:`telegram.Update.business_message`. + + .. versionadded:: 21.1""" + + 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:: 21.1 + """ + + 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:: 21.1 + """ class User(_ChatUserBaseFilter): @@ -2677,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. @@ -2758,7 +2830,9 @@ def filter(self, message: Message) -> bool: VIA_BOT = _ViaBot(name="filters.VIA_BOT") -"""This filter filters for message that were sent via *any* bot.""" +"""This filter filters for message that were sent via *any* bot. + +.. seealso:: :class:`~telegram.ext.filters.ViaBot`""" class _Video(MessageFilter): diff --git a/tests/_files/test_inputsticker.py b/tests/_files/test_inputsticker.py index c974853f268..680a0167099 100644 --- a/tests/_files/test_inputsticker.py +++ b/tests/_files/test_inputsticker.py @@ -33,6 +33,7 @@ def input_sticker(): emoji_list=TestInputStickerBase.emoji_list, mask_position=TestInputStickerBase.mask_position, keywords=TestInputStickerBase.keywords, + format=TestInputStickerBase.format, ) @@ -41,9 +42,10 @@ class TestInputStickerBase: emoji_list = ("👍", "👎") mask_position = MaskPosition("forehead", 0.5, 0.5, 0.5) keywords = ("thumbsup", "thumbsdown") + format = "static" -class TestInputStickerNoRequest(TestInputStickerBase): +class TestInputStickerWithoutRequest(TestInputStickerBase): def test_slot_behaviour(self, input_sticker): inst = input_sticker for attr in inst.__slots__: @@ -56,11 +58,12 @@ def test_expected_values(self, input_sticker): assert input_sticker.emoji_list == self.emoji_list assert input_sticker.mask_position == self.mask_position assert input_sticker.keywords == self.keywords + assert input_sticker.format == self.format def test_attributes_tuple(self, input_sticker): assert isinstance(input_sticker.keywords, tuple) assert isinstance(input_sticker.emoji_list, tuple) - a = InputSticker("sticker", ["emoji"]) + a = InputSticker("sticker", ["emoji"], "static") assert isinstance(a.emoji_list, tuple) assert a.keywords == () @@ -72,9 +75,10 @@ def test_to_dict(self, input_sticker): assert input_sticker_dict["emoji_list"] == list(input_sticker.emoji_list) assert input_sticker_dict["mask_position"] == input_sticker.mask_position.to_dict() assert input_sticker_dict["keywords"] == list(input_sticker.keywords) + assert input_sticker_dict["format"] == input_sticker.format def test_with_sticker_input_types(self, video_sticker_file): # noqa: F811 - sticker = InputSticker(sticker=video_sticker_file, emoji_list=["👍"]) + sticker = InputSticker(sticker=video_sticker_file, emoji_list=["👍"], format="video") assert isinstance(sticker.sticker, InputFile) - sticker = InputSticker(data_file("telegram_video_sticker.webm"), ["👍"]) + sticker = InputSticker(data_file("telegram_video_sticker.webm"), ["👍"], "video") assert sticker.sticker == data_file("telegram_video_sticker.webm").as_uri() diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index 86a2da710c0..c408468118a 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -39,6 +39,7 @@ from telegram.constants import ParseMode, StickerFormat, StickerType from telegram.error import BadRequest, TelegramError from telegram.request import RequestData +from telegram.warnings import PTBDeprecationWarning from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, @@ -574,8 +575,6 @@ def sticker_set_thumb_file(): class TestStickerSetBase: title = "Test stickers" - is_animated = True - is_video = True stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True, Sticker.REGULAR)] name = "NOTAREALNAME" sticker_type = Sticker.REGULAR @@ -584,7 +583,7 @@ class TestStickerSetBase: class TestStickerSetWithoutRequest(TestStickerSetBase): def test_slot_behaviour(self): - inst = StickerSet("this", "is", True, self.stickers, True, "not") + inst = StickerSet("this", "is", self.stickers, "not") for attr in inst.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" @@ -594,8 +593,6 @@ def test_de_json(self, bot, sticker): json_dict = { "name": name, "title": self.title, - "is_animated": self.is_animated, - "is_video": self.is_video, "stickers": [x.to_dict() for x in self.stickers], "thumbnail": sticker.thumbnail.to_dict(), "sticker_type": self.sticker_type, @@ -605,8 +602,6 @@ def test_de_json(self, bot, sticker): assert sticker_set.name == name assert sticker_set.title == self.title - assert sticker_set.is_animated == self.is_animated - assert sticker_set.is_video == self.is_video assert sticker_set.stickers == tuple(self.stickers) assert sticker_set.thumbnail == sticker.thumbnail assert sticker_set.sticker_type == self.sticker_type @@ -618,8 +613,6 @@ def test_sticker_set_to_dict(self, sticker_set): assert isinstance(sticker_set_dict, dict) assert sticker_set_dict["name"] == sticker_set.name assert sticker_set_dict["title"] == sticker_set.title - assert sticker_set_dict["is_animated"] == sticker_set.is_animated - assert sticker_set_dict["is_video"] == sticker_set.is_video assert sticker_set_dict["stickers"][0] == sticker_set.stickers[0].to_dict() assert sticker_set_dict["thumbnail"] == sticker_set.thumbnail.to_dict() assert sticker_set_dict["sticker_type"] == sticker_set.sticker_type @@ -628,26 +621,20 @@ def test_equality(self): a = StickerSet( self.name, self.title, - self.is_animated, self.stickers, - self.is_video, self.sticker_type, ) b = StickerSet( self.name, self.title, - self.is_animated, self.stickers, - self.is_video, self.sticker_type, ) - c = StickerSet(self.name, "title", False, [], True, Sticker.CUSTOM_EMOJI) + c = StickerSet(self.name, "title", [], Sticker.CUSTOM_EMOJI) d = StickerSet( "blah", self.title, - self.is_animated, self.stickers, - self.is_video, self.sticker_type, ) e = Audio(self.name, "", 0, None, None) @@ -685,7 +672,9 @@ async def make_assertion(_, data, *args, **kwargs): ) monkeypatch.setattr(bot, "_post", make_assertion) - await bot.upload_sticker_file(chat_id, sticker=file, sticker_format="static") + await bot.upload_sticker_file( + chat_id, sticker=file, sticker_format=StickerFormat.STATIC + ) assert test_flag finally: bot._local_mode = False @@ -715,8 +704,7 @@ async def make_assertion(_, data, *args, **kwargs): chat_id, "name", "title", - stickers=[InputSticker(file, emoji_list=["emoji"])], - sticker_format=StickerFormat.STATIC, + stickers=[InputSticker(file, emoji_list=["emoji"], format=StickerFormat.STATIC)], ) assert test_flag @@ -755,7 +743,9 @@ async def make_assertion(_, data, *args, **kwargs): monkeypatch.setattr(bot, "_post", make_assertion) await bot.add_sticker_to_set( - chat_id, "name", sticker=InputSticker(sticker=file, emoji_list=["this"]) + chat_id, + "name", + sticker=InputSticker(sticker=file, emoji_list=["this"], format="static"), ) assert test_flag @@ -778,7 +768,7 @@ async def make_assertion(_, data, *args, **kwargs): test_flag = isinstance(data.get("thumbnail"), InputFile) monkeypatch.setattr(bot, "_post", make_assertion) - await bot.set_sticker_set_thumbnail("name", chat_id, thumbnail=file) + await bot.set_sticker_set_thumbnail("name", chat_id, thumbnail=file, format="static") assert test_flag finally: bot._local_mode = False @@ -794,6 +784,27 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(sticker.get_bot(), "get_file", make_assertion) assert await sticker.get_file() + async def test_create_new_sticker_set_format_arg_depr( + self, bot, chat_id, sticker_file, monkeypatch + ): + async def make_assertion(*_, **kwargs): + pass + + monkeypatch.setattr(bot, "_post", make_assertion) + with pytest.warns(PTBDeprecationWarning, match="`sticker_format` is deprecated"): + await bot.create_new_sticker_set( + chat_id, + "name", + "title", + stickers=sticker_file, + sticker_format="static", + ) + + async def test_deprecation_creation_args(self, recwarn): + with pytest.warns(PTBDeprecationWarning, match="The parameters `is_animated` and ") as w: + StickerSet("name", "title", [], "static", is_animated=True) + assert w[0].filename == __file__, "wrong stacklevel!" + @pytest.mark.xdist_group("stickerset") class TestStickerSetWithRequest: @@ -817,8 +828,11 @@ async def test_create_sticker_set( chat_id, name=sticker_set, title="Sticker Test", - stickers=[InputSticker(sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.STATIC, + stickers=[ + InputSticker( + sticker_file, emoji_list=["😄"], format=StickerFormat.STATIC + ) + ], ) assert s elif sticker_set.startswith("animated"): @@ -826,8 +840,13 @@ async def test_create_sticker_set( chat_id, name=sticker_set, title="Animated Test", - stickers=[InputSticker(animated_sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.ANIMATED, + stickers=[ + InputSticker( + animated_sticker_file, + emoji_list=["😄"], + format=StickerFormat.ANIMATED, + ) + ], ) assert a elif sticker_set.startswith("video"): @@ -835,8 +854,11 @@ async def test_create_sticker_set( chat_id, name=sticker_set, title="Video Test", - stickers=[InputSticker(video_sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.VIDEO, + stickers=[ + InputSticker( + video_sticker_file, emoji_list=["😄"], format=StickerFormat.VIDEO + ) + ], ) assert v @@ -850,8 +872,7 @@ async def test_delete_sticker_set(self, bot, chat_id, sticker_file): chat_id, name=name, title="Stickerset delete Test", - stickers=[InputSticker(sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.STATIC, + stickers=[InputSticker(sticker_file, emoji_list=["😄"], format="static")], ) # this prevents a second issue when calling delete too soon after creating the set leads # to it failing as well @@ -870,8 +891,11 @@ async def test_set_custom_emoji_sticker_set_thumbnail( chat_id, name=ss_name, title="Custom Emoji Sticker Set", - stickers=[InputSticker(animated_sticker_file, emoji_list=["😄"])], - sticker_format=StickerFormat.ANIMATED, + stickers=[ + InputSticker( + animated_sticker_file, emoji_list=["😄"], format=StickerFormat.ANIMATED + ) + ], sticker_type=Sticker.CUSTOM_EMOJI, ) assert await bot.set_custom_emoji_sticker_set_thumbnail(ss_name, "") @@ -890,7 +914,9 @@ async def test_bot_methods_1_png(self, bot, chat_id, sticker_file): bot.add_sticker_to_set( chat_id, f"test_by_{bot.username}", - sticker=InputSticker(sticker=file.file_id, emoji_list=["😄"]), + sticker=InputSticker( + sticker=file.file_id, emoji_list=["😄"], format=StickerFormat.STATIC + ), ), bot.add_sticker_to_set( # Also test with file input and mask chat_id, @@ -899,6 +925,7 @@ async def test_bot_methods_1_png(self, bot, chat_id, sticker_file): sticker=sticker_file, emoji_list=["😄"], mask_position=MaskPosition(MaskPosition.EYES, -1, 1, 2), + format=StickerFormat.STATIC, ), ), ) @@ -910,7 +937,9 @@ async def test_bot_methods_1_tgs(self, bot, chat_id): chat_id, f"animated_test_by_{bot.username}", sticker=InputSticker( - sticker=data_file("telegram_animated_sticker.tgs").open("rb"), emoji_list=["😄"] + sticker=data_file("telegram_animated_sticker.tgs").open("rb"), + emoji_list=["😄"], + format=StickerFormat.ANIMATED, ), ) @@ -920,7 +949,7 @@ async def test_bot_methods_1_webm(self, bot, chat_id): assert await bot.add_sticker_to_set( chat_id, f"video_test_by_{bot.username}", - sticker=InputSticker(sticker=f, emoji_list=["🤔"]), + sticker=InputSticker(sticker=f, emoji_list=["🤔"], format=StickerFormat.VIDEO), ) # Test set_sticker_position_in_set @@ -943,7 +972,7 @@ async def test_bot_methods_2_webm(self, bot, video_sticker_set): async def test_bot_methods_3_png(self, bot, chat_id, sticker_set_thumb_file): await asyncio.sleep(1) assert await bot.set_sticker_set_thumbnail( - f"test_by_{bot.username}", chat_id, sticker_set_thumb_file + f"test_by_{bot.username}", chat_id, format="static", thumbnail=sticker_set_thumb_file ) async def test_bot_methods_3_tgs( @@ -953,8 +982,13 @@ async def test_bot_methods_3_tgs( animated_test = f"animated_test_by_{bot.username}" file_id = animated_sticker_set.stickers[-1].file_id tasks = asyncio.gather( - bot.set_sticker_set_thumbnail(animated_test, chat_id, animated_sticker_file), - bot.set_sticker_set_thumbnail(animated_test, chat_id, file_id), + bot.set_sticker_set_thumbnail( + animated_test, + chat_id, + "animated", + thumbnail=animated_sticker_file, + ), + bot.set_sticker_set_thumbnail(animated_test, chat_id, "animated", thumbnail=file_id), ) assert all(await tasks) @@ -1037,6 +1071,19 @@ async def test_bot_methods_7_webm(self, bot, video_sticker_set): file_id = video_sticker_set.stickers[-1].file_id assert await bot.set_sticker_keywords(file_id, ["test", "test2"]) + async def test_bot_methods_8_png(self, bot, sticker_set, sticker_file): + file_id = sticker_set.stickers[-1].file_id + assert await bot.replace_sticker_in_set( + bot.id, + f"test_by_{bot.username}", + file_id, + sticker=InputSticker( + sticker=sticker_file, + emoji_list=["😄"], + format=StickerFormat.STATIC, + ), + ) + @pytest.fixture(scope="module") def mask_position(): @@ -1126,9 +1173,9 @@ async def test_create_new_mask_sticker_set(self, bot, chat_id, sticker_file, mas emoji_list=["😔"], mask_position=mask_position, keywords=["sad"], + format=StickerFormat.STATIC, ) ], - sticker_format=StickerFormat.STATIC, sticker_type=Sticker.MASK, ) assert sticker_set diff --git a/tests/conftest.py b/tests/conftest.py index c7e816672ae..213bcff4a23 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -73,9 +73,7 @@ def no_rerun_after_xfail_or_flood(error, name, test: pytest.Function, plugin): if getattr(error[1], "msg", "") is None: raise error[1] did_we_flood = "flood" in getattr(error[1], "msg", "") # _pytest.outcomes.XFailed has 'msg' - if xfail_present or did_we_flood: - return False - return True + return not (xfail_present or did_we_flood) def pytest_collection_modifyitems(items: List[pytest.Item]): diff --git a/tests/ext/test_businessconnectionhandler.py b/tests/ext/test_businessconnectionhandler.py new file mode 100644 index 00000000000..c8d741332a4 --- /dev/null +++ b/tests/ext/test_businessconnectionhandler.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import asyncio +import datetime + +import pytest + +from telegram import ( + Bot, + BusinessConnection, + CallbackQuery, + Chat, + ChosenInlineResult, + Message, + PreCheckoutQuery, + ShippingQuery, + Update, + User, +) +from telegram._utils.datetime import UTC +from telegram.ext import BusinessConnectionHandler, CallbackContext, JobQueue +from tests.auxil.slots import mro_slots + +message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") + +params = [ + {"message": message}, + {"edited_message": message}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, + {"channel_post": message}, + {"edited_channel_post": message}, + {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, + {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, + {"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, +] + +ids = ( + "message", + "edited_message", + "callback_query", + "channel_post", + "edited_channel_post", + "chosen_inline_result", + "shipping_query", + "pre_checkout_query", + "callback_query_without_message", +) + + +@pytest.fixture(scope="class", params=params, ids=ids) +def false_update(request): + return Update(update_id=2, **request.param) + + +@pytest.fixture(scope="class") +def time(): + return datetime.datetime.now(tz=UTC) + + +@pytest.fixture(scope="class") +def business_connection(bot): + bc = BusinessConnection( + id="1", + user_chat_id=1, + user=User(1, "name", username="user_a", is_bot=False), + date=datetime.datetime.now(tz=UTC), + can_reply=True, + is_enabled=True, + ) + bc.set_bot(bot) + return bc + + +@pytest.fixture() +def business_connection_update(bot, business_connection): + return Update(0, business_connection=business_connection) + + +class TestBusinessConnectionHandler: + test_flag = False + + def test_slot_behaviour(self): + action = BusinessConnectionHandler(self.callback) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + @pytest.fixture(autouse=True) + def _reset(self): + self.test_flag = False + + async def callback(self, update, context): + self.test_flag = ( + isinstance(context, CallbackContext) + and isinstance(context.bot, Bot) + and isinstance(update, Update) + and isinstance(context.update_queue, asyncio.Queue) + and isinstance(context.job_queue, JobQueue) + and isinstance(context.user_data, dict) + and isinstance(context.bot_data, dict) + and isinstance( + update.business_connection, + BusinessConnection, + ) + ) + + def test_with_user_id(self, business_connection_update): + handler = BusinessConnectionHandler(self.callback, user_id=1) + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, user_id=[1]) + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, user_id=2, username="@user_a") + assert handler.check_update(business_connection_update) + + handler = BusinessConnectionHandler(self.callback, user_id=2) + assert not handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, user_id=[2]) + assert not handler.check_update(business_connection_update) + + def test_with_username(self, business_connection_update): + handler = BusinessConnectionHandler(self.callback, username="user_a") + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username="@user_a") + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username=["user_a"]) + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username=["@user_a"]) + assert handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, user_id=1, username="@user_b") + assert handler.check_update(business_connection_update) + + handler = BusinessConnectionHandler(self.callback, username="user_b") + assert not handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username="@user_b") + assert not handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username=["user_b"]) + assert not handler.check_update(business_connection_update) + handler = BusinessConnectionHandler(self.callback, username=["@user_b"]) + assert not handler.check_update(business_connection_update) + + business_connection_update.business_connection.user._unfreeze() + business_connection_update.business_connection.user.username = None + assert not handler.check_update(business_connection_update) + + def test_other_update_types(self, false_update): + handler = BusinessConnectionHandler(self.callback) + assert not handler.check_update(false_update) + assert not handler.check_update(True) + + async def test_context(self, app, business_connection_update): + handler = BusinessConnectionHandler(callback=self.callback) + app.add_handler(handler) + + async with app: + await app.process_update(business_connection_update) + assert self.test_flag diff --git a/tests/ext/test_businessmessagesdeletedhandler.py b/tests/ext/test_businessmessagesdeletedhandler.py new file mode 100644 index 00000000000..a15a0a0c2b4 --- /dev/null +++ b/tests/ext/test_businessmessagesdeletedhandler.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import asyncio +import datetime + +import pytest + +from telegram import ( + Bot, + BusinessMessagesDeleted, + CallbackQuery, + Chat, + ChosenInlineResult, + Message, + PreCheckoutQuery, + ShippingQuery, + Update, + User, +) +from telegram._utils.datetime import UTC +from telegram.ext import BusinessMessagesDeletedHandler, CallbackContext, JobQueue +from tests.auxil.slots import mro_slots + +message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") + +params = [ + {"message": message}, + {"edited_message": message}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, + {"channel_post": message}, + {"edited_channel_post": message}, + {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, + {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, + {"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, +] + +ids = ( + "message", + "edited_message", + "callback_query", + "channel_post", + "edited_channel_post", + "chosen_inline_result", + "shipping_query", + "pre_checkout_query", + "callback_query_without_message", +) + + +@pytest.fixture(scope="class", params=params, ids=ids) +def false_update(request): + return Update(update_id=2, **request.param) + + +@pytest.fixture(scope="class") +def time(): + return datetime.datetime.now(tz=UTC) + + +@pytest.fixture(scope="class") +def business_messages_deleted(bot): + bmd = BusinessMessagesDeleted( + business_connection_id="1", + chat=Chat(1, Chat.PRIVATE, username="user_a"), + message_ids=[1, 2, 3], + ) + bmd.set_bot(bot) + return bmd + + +@pytest.fixture() +def business_messages_deleted_update(bot, business_messages_deleted): + return Update(0, deleted_business_messages=business_messages_deleted) + + +class TestBusinessMessagesDeletedHandler: + test_flag = False + + def test_slot_behaviour(self): + action = BusinessMessagesDeletedHandler(self.callback) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + @pytest.fixture(autouse=True) + def _reset(self): + self.test_flag = False + + async def callback(self, update, context): + self.test_flag = ( + isinstance(context, CallbackContext) + and isinstance(context.bot, Bot) + and isinstance(update, Update) + and isinstance(context.update_queue, asyncio.Queue) + and isinstance(context.job_queue, JobQueue) + and isinstance(context.chat_data, dict) + and isinstance(context.bot_data, dict) + and isinstance( + update.deleted_business_messages, + BusinessMessagesDeleted, + ) + ) + + def test_with_chat_id(self, business_messages_deleted_update): + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=1) + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=[1]) + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=2, username="@user_a") + assert handler.check_update(business_messages_deleted_update) + + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=2) + assert not handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=[2]) + assert not handler.check_update(business_messages_deleted_update) + + def test_with_username(self, business_messages_deleted_update): + handler = BusinessMessagesDeletedHandler(self.callback, username="user_a") + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username="@user_a") + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username=["user_a"]) + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username=["@user_a"]) + assert handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, chat_id=1, username="@user_b") + assert handler.check_update(business_messages_deleted_update) + + handler = BusinessMessagesDeletedHandler(self.callback, username="user_b") + assert not handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username="@user_b") + assert not handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username=["user_b"]) + assert not handler.check_update(business_messages_deleted_update) + handler = BusinessMessagesDeletedHandler(self.callback, username=["@user_b"]) + assert not handler.check_update(business_messages_deleted_update) + + business_messages_deleted_update.deleted_business_messages.chat._unfreeze() + business_messages_deleted_update.deleted_business_messages.chat.username = None + assert not handler.check_update(business_messages_deleted_update) + + def test_other_update_types(self, false_update): + handler = BusinessMessagesDeletedHandler(self.callback) + assert not handler.check_update(false_update) + assert not handler.check_update(True) + + async def test_context(self, app, business_messages_deleted_update): + handler = BusinessMessagesDeletedHandler(callback=self.callback) + app.add_handler(handler) + + async with app: + await app.process_update(business_messages_deleted_update) + assert self.test_flag diff --git a/tests/ext/test_callbackcontext.py b/tests/ext/test_callbackcontext.py index ed4f014b2b2..0a099f64f15 100644 --- a/tests/ext/test_callbackcontext.py +++ b/tests/ext/test_callbackcontext.py @@ -32,6 +32,7 @@ from telegram.error import TelegramError from telegram.ext import ApplicationBuilder, CallbackContext, Job from telegram.warnings import PTBUserWarning +from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots """ @@ -211,8 +212,9 @@ def test_drop_callback_data_exception(self, bot, app): finally: app.bot = bot - async def test_drop_callback_data(self, bot, monkeypatch, chat_id): - app = ApplicationBuilder().token(bot.token).arbitrary_callback_data(True).build() + async def test_drop_callback_data(self, bot, chat_id): + new_bot = make_bot(token=bot.token, arbitrary_callback_data=True) + app = ApplicationBuilder().bot(new_bot).build() update = Update( 0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False)) diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index bcd1980914e..694ea009a6f 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -2035,6 +2035,11 @@ def test_filters_is_automatic_forward(self, update): update.message.is_automatic_forward = True assert filters.IS_AUTOMATIC_FORWARD.check_update(update) + def test_filters_is_from_offline(self, update): + assert not filters.IS_FROM_OFFLINE.check_update(update) + update.message.is_from_offline = True + assert filters.IS_FROM_OFFLINE.check_update(update) + def test_filters_is_topic_message(self, update): assert not filters.IS_TOPIC_MESSAGE.check_update(update) update.message.is_topic_message = True @@ -2343,6 +2348,9 @@ def test_update_type_message(self, update): assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) assert not filters.UpdateType.EDITED.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_edited_message(self, update): update.edited_message, update.message = update.message, update.edited_message @@ -2353,6 +2361,9 @@ def test_update_type_edited_message(self, update): assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) assert filters.UpdateType.EDITED.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_channel_post(self, update): update.channel_post, update.message = update.message, update.edited_message @@ -2363,6 +2374,9 @@ def test_update_type_channel_post(self, update): assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert filters.UpdateType.CHANNEL_POSTS.check_update(update) assert not filters.UpdateType.EDITED.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_edited_channel_post(self, update): update.edited_channel_post, update.message = update.message, update.edited_message @@ -2373,6 +2387,35 @@ def test_update_type_edited_channel_post(self, update): assert filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert filters.UpdateType.CHANNEL_POSTS.check_update(update) assert filters.UpdateType.EDITED.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) + + def test_update_type_business_message(self, update): + update.business_message, update.message = update.message, update.edited_message + assert not filters.UpdateType.MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) + assert not filters.UpdateType.MESSAGES.check_update(update) + assert not filters.UpdateType.CHANNEL_POST.check_update(update) + assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) + assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) + assert not filters.UpdateType.EDITED.check_update(update) + assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) + + def test_update_type_edited_business_message(self, update): + update.edited_business_message, update.message = update.message, update.edited_message + assert not filters.UpdateType.MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) + assert not filters.UpdateType.MESSAGES.check_update(update) + assert not filters.UpdateType.CHANNEL_POST.check_update(update) + assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) + assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) + assert filters.UpdateType.EDITED.check_update(update) + assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update) + assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) + assert filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_merged_short_circuit_and(self, update, base_class): update.message.text = "/test" diff --git a/tests/ext/test_updater.py b/tests/ext/test_updater.py index 0c81144aedd..feed189c662 100644 --- a/tests/ext/test_updater.py +++ b/tests/ext/test_updater.py @@ -38,7 +38,16 @@ from tests.auxil.pytest_classes import PytestBot, make_bot from tests.auxil.slots import mro_slots +UNIX_AVAILABLE = False + if TEST_WITH_OPT_DEPS: + try: + from tornado.netutil import bind_unix_socket + + UNIX_AVAILABLE = True + except ImportError: + UNIX_AVAILABLE = False + from telegram.ext._utils.webhookhandler import WebhookServer @@ -692,13 +701,12 @@ async def delete_webhook(*args, **kwargs): @pytest.mark.parametrize("ext_bot", [True, False]) @pytest.mark.parametrize("drop_pending_updates", [True, False]) @pytest.mark.parametrize("secret_token", ["SecretToken", None]) - @pytest.mark.parametrize("unix", [None, True]) + @pytest.mark.parametrize( + "unix", [None, "file_path", "socket_object"] if UNIX_AVAILABLE else [None] + ) async def test_webhook_basic( self, monkeypatch, updater, drop_pending_updates, ext_bot, secret_token, unix, file_path ): - # Skipping unix test on windows since they fail - if unix and platform.system() == "Windows": - pytest.skip("Windows doesn't support unix bind") # Testing with both ExtBot and Bot to make sure any logic in WebhookHandler # that depends on this distinction works if ext_bot and not isinstance(updater.bot, ExtBot): @@ -723,11 +731,12 @@ async def set_webhook(*args, **kwargs): async with updater: if unix: + socket = file_path if unix == "file_path" else bind_unix_socket(file_path) return_value = await updater.start_webhook( drop_pending_updates=drop_pending_updates, secret_token=secret_token, url_path="TOKEN", - unix=file_path, + unix=socket, webhook_url="string", ) else: @@ -815,10 +824,11 @@ async def set_webhook(*args, **kwargs): # We call the same logic twice to make sure that restarting the updater works as well if unix: + socket = file_path if unix == "file_path" else bind_unix_socket(file_path) await updater.start_webhook( drop_pending_updates=drop_pending_updates, secret_token=secret_token, - unix=file_path, + unix=socket, webhook_url="string", ) else: @@ -1039,7 +1049,7 @@ async def return_true(*args, **kwargs): assert updater.running is False async def test_webhook_ssl_just_for_telegram(self, monkeypatch, updater): - """Here we just test that the SSL info is pased to Telegram, but __not__ to the the + """Here we just test that the SSL info is pased to Telegram, but __not__ to the webhook server""" async def set_webhook(**kwargs): diff --git a/tests/request/test_requestparameter.py b/tests/request/test_requestparameter.py index 59a3e3f9145..d7ad2088a73 100644 --- a/tests/request/test_requestparameter.py +++ b/tests/request/test_requestparameter.py @@ -163,7 +163,7 @@ def test_from_input_inputmedia_without_attach(self): assert request_parameter.input_files == [input_media.media, input_media.thumbnail] def test_from_input_inputsticker(self): - input_sticker = InputSticker(data_file("telegram.png").read_bytes(), ["emoji"]) + input_sticker = InputSticker(data_file("telegram.png").read_bytes(), ["emoji"], "static") expected = input_sticker.to_dict() expected.update({"sticker": input_sticker.sticker.attach_uri}) request_parameter = RequestParameter.from_input("key", input_sticker) diff --git a/tests/test_birthdate.py b/tests/test_birthdate.py new file mode 100644 index 00000000000..4c028661ac8 --- /dev/null +++ b/tests/test_birthdate.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +from datetime import datetime + +import pytest + +from telegram import Birthdate +from tests.auxil.slots import mro_slots + + +class TestBirthdateBase: + day = 1 + month = 1 + year = 2022 + + +@pytest.fixture(scope="module") +def birthdate(): + return Birthdate(TestBirthdateBase.day, TestBirthdateBase.month, TestBirthdateBase.year) + + +class TestBirthdateWithoutRequest(TestBirthdateBase): + def test_slot_behaviour(self, birthdate): + for attr in birthdate.__slots__: + assert getattr(birthdate, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(birthdate)) == len(set(mro_slots(birthdate))), "duplicate slot" + + def test_to_dict(self, birthdate): + bd_dict = birthdate.to_dict() + assert isinstance(bd_dict, dict) + assert bd_dict["day"] == self.day + assert bd_dict["month"] == self.month + assert bd_dict["year"] == self.year + + def test_de_json(self, bot): + json_dict = {"day": self.day, "month": self.month, "year": self.year} + bd = Birthdate.de_json(json_dict, bot) + assert isinstance(bd, Birthdate) + assert bd.day == self.day + assert bd.month == self.month + assert bd.year == self.year + + def test_equality(self): + bd1 = Birthdate(1, 1, 2022) + bd2 = Birthdate(1, 1, 2022) + bd3 = Birthdate(1, 1, 2023) + bd4 = Birthdate(1, 2, 2022) + + assert bd1 == bd2 + assert hash(bd1) == hash(bd2) + + assert bd1 == bd3 + assert hash(bd1) == hash(bd3) + + assert bd1 != bd4 + assert hash(bd1) != hash(bd4) + + def test_to_date(self, birthdate): + assert isinstance(birthdate.to_date(), datetime) + assert birthdate.to_date() == datetime(self.year, self.month, self.day) + new_bd = birthdate.to_date(2023) + assert new_bd == datetime(2023, self.month, self.day) + + def test_to_date_no_year(self): + bd = Birthdate(1, 1) + with pytest.raises(ValueError, match="The `year` argument is required"): + bd.to_date() diff --git a/tests/test_bot.py b/tests/test_bot.py index 71e5b23041d..7021867da64 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -22,7 +22,6 @@ import inspect import logging import pickle -import re import socket import time from collections import defaultdict @@ -40,6 +39,7 @@ BotDescription, BotName, BotShortDescription, + BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -388,22 +388,10 @@ def test_bot_deepcopy_error(self, bot): with pytest.raises(TypeError, match="Bot objects cannot be deepcopied"): copy.deepcopy(bot) - @bot_methods(ext_bot=False) - def test_api_methods_have_log_decorator(self, bot_class, bot_method_name, bot_method): - """Check that all bot methods have the log decorator ...""" - # not islower() skips the camelcase aliases - if not bot_method_name.islower(): - return - source = inspect.getsource(bot_method) - assert ( - # Use re.match to only match at *the beginning* of the string - re.match(rf"\s*\@\_log\s*async def {bot_method_name}", source) - ), f"{bot_method_name} is missing the @_log decorator" - @pytest.mark.parametrize( ("cls", "logger_name"), [(Bot, "telegram.Bot"), (ExtBot, "telegram.ext.ExtBot")] ) - async def test_log_decorator(self, bot: PytestExtBot, cls, logger_name, caplog): + async def test_bot_method_logging(self, bot: PytestExtBot, cls, logger_name, caplog): # Second argument makes sure that we ignore logs from e.g. httpx with caplog.at_level(logging.DEBUG, logger="telegram"): await cls(bot.token).get_me() @@ -415,11 +403,19 @@ async def test_log_decorator(self, bot: PytestExtBot, cls, logger_name, caplog): caplog.records.pop(idx) if record.getMessage().startswith("Task exception was never retrieved"): caplog.records.pop(idx) - assert len(caplog.records) == 3 + assert len(caplog.records) == 2 assert all(caplog.records[i].name == logger_name for i in [-1, 0]) - assert caplog.records[0].getMessage().startswith("Entering: get_me") - assert caplog.records[-1].getMessage().startswith("Exiting: get_me") + assert ( + caplog.records[0] + .getMessage() + .startswith("Calling Bot API endpoint `getMe` with parameters `{}`") + ) + assert ( + caplog.records[-1] + .getMessage() + .startswith("Call to Bot API endpoint `getMe` finished with return value") + ) @bot_methods() def test_camel_case_aliases(self, bot_class, bot_method_name, bot_method): @@ -2092,6 +2088,37 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): api_kwargs={"chat_id": 2, "user_id": 32, "until_date": until_timestamp}, ) + async def test_business_connection_id_argument(self, bot, monkeypatch): + """We can't connect to a business acc, so we just test that the correct data is passed. + We also can't test every single method easily, so we just test one. Our linting will catch + any unused args with the others.""" + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters.get("business_connection_id") == 42 + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.send_message(2, "text", business_connection_id=42) + + async def test_get_business_connection(self, bot, monkeypatch): + bci = "42" + user = User(1, "first", False) + user_chat_id = 1 + date = dtm.datetime.utcnow() + can_reply = True + is_enabled = True + bc = BusinessConnection(bci, user, user_chat_id, date, can_reply, is_enabled).to_json() + + async def do_request(*args, **kwargs): + data = kwargs.get("request_data") + obj = data.parameters.get("business_connection_id") + if obj == bci: + return 200, f'{{"ok": true, "result": {bc}}}'.encode() + return 400, b'{"ok": false, "result": []}' + + monkeypatch.setattr(bot.request, "do_request", do_request) + obj = await bot.get_business_connection(business_connection_id=bci) + assert isinstance(obj, BusinessConnection) + class TestBotWithRequest: """ @@ -3378,8 +3405,8 @@ async def test_pin_and_unpin_message(self, bot, super_group_id): assert await bot.unpin_all_chat_messages(super_group_id, read_timeout=10) # get_sticker_set, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, - # set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers - # are tested in the test_sticker module. + # set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers, + # replace_sticker_in_set are tested in the test_sticker module. # get_forum_topic_icon_stickers, edit_forum_topic, general_forum etc... # are tested in the test_forum module. diff --git a/tests/test_business.py b/tests/test_business.py new file mode 100644 index 00000000000..da6838d6d47 --- /dev/null +++ b/tests/test_business.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +from datetime import datetime + +import pytest + +from telegram import ( + BusinessConnection, + BusinessIntro, + BusinessLocation, + BusinessMessagesDeleted, + BusinessOpeningHours, + BusinessOpeningHoursInterval, + Chat, + Location, + Sticker, + User, +) +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + + +class TestBusinessBase: + id_ = "123" + user = User(123, "test_user", False) + user_chat_id = 123 + date = datetime.now(tz=UTC).replace(microsecond=0) + can_reply = True + is_enabled = True + message_ids = (123, 321) + business_connection_id = "123" + chat = Chat(123, "test_chat") + title = "Business Title" + message = "Business description" + sticker = Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR) + address = "address" + location = Location(-23.691288, 46.788279) + opening_minute = 0 + closing_minute = 60 + time_zone_name = "Country/City" + opening_hours = [ + BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60) + ] + + +@pytest.fixture(scope="module") +def business_connection(): + return BusinessConnection( + TestBusinessBase.id_, + TestBusinessBase.user, + TestBusinessBase.user_chat_id, + TestBusinessBase.date, + TestBusinessBase.can_reply, + TestBusinessBase.is_enabled, + ) + + +@pytest.fixture(scope="module") +def business_messages_deleted(): + return BusinessMessagesDeleted( + TestBusinessBase.business_connection_id, + TestBusinessBase.chat, + TestBusinessBase.message_ids, + ) + + +@pytest.fixture(scope="module") +def business_intro(): + return BusinessIntro( + TestBusinessBase.title, + TestBusinessBase.message, + TestBusinessBase.sticker, + ) + + +@pytest.fixture(scope="module") +def business_location(): + return BusinessLocation( + TestBusinessBase.address, + TestBusinessBase.location, + ) + + +@pytest.fixture(scope="module") +def business_opening_hours_interval(): + return BusinessOpeningHoursInterval( + TestBusinessBase.opening_minute, + TestBusinessBase.closing_minute, + ) + + +@pytest.fixture(scope="module") +def business_opening_hours(): + return BusinessOpeningHours( + TestBusinessBase.time_zone_name, + TestBusinessBase.opening_hours, + ) + + +class TestBusinessConnectionWithoutRequest(TestBusinessBase): + def test_slots(self, business_connection): + bc = business_connection + for attr in bc.__slots__: + assert getattr(bc, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bc)) == len(set(mro_slots(bc))), "duplicate slot" + + def test_de_json(self): + json_dict = { + "id": self.id_, + "user": self.user.to_dict(), + "user_chat_id": self.user_chat_id, + "date": to_timestamp(self.date), + "can_reply": self.can_reply, + "is_enabled": self.is_enabled, + } + bc = BusinessConnection.de_json(json_dict, None) + assert bc.id == self.id_ + assert bc.user == self.user + assert bc.user_chat_id == self.user_chat_id + assert bc.date == self.date + assert bc.can_reply == self.can_reply + assert bc.is_enabled == self.is_enabled + assert bc.api_kwargs == {} + assert isinstance(bc, BusinessConnection) + + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "id": self.id_, + "user": self.user.to_dict(), + "user_chat_id": self.user_chat_id, + "date": to_timestamp(self.date), + "can_reply": self.can_reply, + "is_enabled": self.is_enabled, + } + chat_bot = BusinessConnection.de_json(json_dict, bot) + chat_bot_raw = BusinessConnection.de_json(json_dict, raw_bot) + chat_bot_tz = BusinessConnection.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing tzinfo objects is not reliable + date_offset = chat_bot_tz.date.utcoffset() + date_offset_tz = tz_bot.defaults.tzinfo.utcoffset(chat_bot_tz.date.replace(tzinfo=None)) + + assert chat_bot.date.tzinfo == UTC + assert chat_bot_raw.date.tzinfo == UTC + assert date_offset_tz == date_offset + + def test_to_dict(self, business_connection): + bc_dict = business_connection.to_dict() + assert isinstance(bc_dict, dict) + assert bc_dict["id"] == self.id_ + assert bc_dict["user"] == self.user.to_dict() + assert bc_dict["user_chat_id"] == self.user_chat_id + assert bc_dict["date"] == to_timestamp(self.date) + assert bc_dict["can_reply"] == self.can_reply + assert bc_dict["is_enabled"] == self.is_enabled + + def test_equality(self): + bc1 = BusinessConnection( + self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + ) + bc2 = BusinessConnection( + self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + ) + bc3 = BusinessConnection( + "321", self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + ) + + assert bc1 == bc2 + assert hash(bc1) == hash(bc2) + + assert bc1 != bc3 + assert hash(bc1) != hash(bc3) + + +class TestBusinessMessagesDeleted(TestBusinessBase): + def test_slots(self, business_messages_deleted): + bmd = business_messages_deleted + for attr in bmd.__slots__: + assert getattr(bmd, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bmd)) == len(set(mro_slots(bmd))), "duplicate slot" + + def test_to_dict(self, business_messages_deleted): + bmd_dict = business_messages_deleted.to_dict() + assert isinstance(bmd_dict, dict) + assert bmd_dict["message_ids"] == list(self.message_ids) + assert bmd_dict["business_connection_id"] == self.business_connection_id + assert bmd_dict["chat"] == self.chat.to_dict() + + def test_de_json(self): + json_dict = { + "business_connection_id": self.business_connection_id, + "chat": self.chat.to_dict(), + "message_ids": self.message_ids, + } + bmd = BusinessMessagesDeleted.de_json(json_dict, None) + assert bmd.business_connection_id == self.business_connection_id + assert bmd.chat == self.chat + assert bmd.message_ids == self.message_ids + assert bmd.api_kwargs == {} + assert isinstance(bmd, BusinessMessagesDeleted) + + def test_equality(self): + bmd1 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids) + bmd2 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids) + bmd3 = BusinessMessagesDeleted("1", Chat(4, "random"), [321, 123]) + + assert bmd1 == bmd2 + assert hash(bmd1) == hash(bmd2) + + assert bmd1 != bmd3 + assert hash(bmd1) != hash(bmd3) + + +class TestBusinessIntroWithoutRequest(TestBusinessBase): + def test_slot_behaviour(self, business_intro): + intro = business_intro + for attr in intro.__slots__: + assert getattr(intro, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(intro)) == len(set(mro_slots(intro))), "duplicate slot" + + def test_to_dict(self, business_intro): + intro_dict = business_intro.to_dict() + assert isinstance(intro_dict, dict) + assert intro_dict["title"] == self.title + assert intro_dict["message"] == self.message + assert intro_dict["sticker"] == self.sticker.to_dict() + + def test_de_json(self): + json_dict = { + "title": self.title, + "message": self.message, + "sticker": self.sticker.to_dict(), + } + intro = BusinessIntro.de_json(json_dict, None) + assert intro.title == self.title + assert intro.message == self.message + assert intro.sticker == self.sticker + assert intro.api_kwargs == {} + assert isinstance(intro, BusinessIntro) + + def test_equality(self): + intro1 = BusinessIntro(self.title, self.message, self.sticker) + intro2 = BusinessIntro(self.title, self.message, self.sticker) + intro3 = BusinessIntro("Other Business", self.message, self.sticker) + + assert intro1 == intro2 + assert hash(intro1) == hash(intro2) + assert intro1 is not intro2 + + assert intro1 != intro3 + assert hash(intro1) != hash(intro3) + + +class TestBusinessLocationWithoutRequest(TestBusinessBase): + def test_slot_behaviour(self, business_location): + inst = business_location + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, business_location): + blc_dict = business_location.to_dict() + assert isinstance(blc_dict, dict) + assert blc_dict["address"] == self.address + assert blc_dict["location"] == self.location.to_dict() + + def test_de_json(self): + json_dict = { + "address": self.address, + "location": self.location.to_dict(), + } + blc = BusinessLocation.de_json(json_dict, None) + assert blc.address == self.address + assert blc.location == self.location + assert blc.api_kwargs == {} + assert isinstance(blc, BusinessLocation) + + def test_equality(self): + blc1 = BusinessLocation(self.address, self.location) + blc2 = BusinessLocation(self.address, self.location) + blc3 = BusinessLocation("Other Address", self.location) + + assert blc1 == blc2 + assert hash(blc1) == hash(blc2) + assert blc1 is not blc2 + + assert blc1 != blc3 + assert hash(blc1) != hash(blc3) + + +class TestBusinessOpeningHoursIntervalWithoutRequest(TestBusinessBase): + def test_slot_behaviour(self, business_opening_hours_interval): + inst = business_opening_hours_interval + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, business_opening_hours_interval): + bohi_dict = business_opening_hours_interval.to_dict() + assert isinstance(bohi_dict, dict) + assert bohi_dict["opening_minute"] == self.opening_minute + assert bohi_dict["closing_minute"] == self.closing_minute + + def test_de_json(self): + json_dict = { + "opening_minute": self.opening_minute, + "closing_minute": self.closing_minute, + } + bohi = BusinessOpeningHoursInterval.de_json(json_dict, None) + assert bohi.opening_minute == self.opening_minute + assert bohi.closing_minute == self.closing_minute + assert bohi.api_kwargs == {} + assert isinstance(bohi, BusinessOpeningHoursInterval) + + def test_equality(self): + bohi1 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute) + bohi2 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute) + bohi3 = BusinessOpeningHoursInterval(61, 100) + + assert bohi1 == bohi2 + assert hash(bohi1) == hash(bohi2) + assert bohi1 is not bohi2 + + assert bohi1 != bohi3 + assert hash(bohi1) != hash(bohi3) + + @pytest.mark.parametrize( + ("opening_minute", "expected"), + [ # openings per docstring + (8 * 60, (0, 8, 0)), + (24 * 60, (1, 0, 0)), + (6 * 24 * 60, (6, 0, 0)), + ], + ) + def test_opening_time(self, opening_minute, expected): + bohi = BusinessOpeningHoursInterval(opening_minute, -0) + + opening_time = bohi.opening_time + assert opening_time == expected + + cached = bohi.opening_time + assert cached is opening_time + + @pytest.mark.parametrize( + ("closing_minute", "expected"), + [ # closings per docstring + (20 * 60 + 30, (0, 20, 30)), + (2 * 24 * 60 - 1, (1, 23, 59)), + (7 * 24 * 60 - 2, (6, 23, 58)), + ], + ) + def test_closing_time(self, closing_minute, expected): + bohi = BusinessOpeningHoursInterval(-0, closing_minute) + + closing_time = bohi.closing_time + assert closing_time == expected + + cached = bohi.closing_time + assert cached is closing_time + + +class TestBusinessOpeningHoursWithoutRequest(TestBusinessBase): + def test_slot_behaviour(self, business_opening_hours): + inst = business_opening_hours + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, business_opening_hours): + boh_dict = business_opening_hours.to_dict() + assert isinstance(boh_dict, dict) + assert boh_dict["time_zone_name"] == self.time_zone_name + assert boh_dict["opening_hours"] == [opening.to_dict() for opening in self.opening_hours] + + def test_de_json(self): + json_dict = { + "time_zone_name": self.time_zone_name, + "opening_hours": [opening.to_dict() for opening in self.opening_hours], + } + boh = BusinessOpeningHours.de_json(json_dict, None) + assert boh.time_zone_name == self.time_zone_name + assert boh.opening_hours == tuple(self.opening_hours) + assert boh.api_kwargs == {} + assert isinstance(boh, BusinessOpeningHours) + + def test_equality(self): + boh1 = BusinessOpeningHours(self.time_zone_name, self.opening_hours) + boh2 = BusinessOpeningHours(self.time_zone_name, self.opening_hours) + boh3 = BusinessOpeningHours("Other/Timezone", self.opening_hours) + + assert boh1 == boh2 + assert hash(boh1) == hash(boh2) + assert boh1 is not boh2 + + assert boh1 != boh3 + assert hash(boh1) != hash(boh3) diff --git a/tests/test_chat.py b/tests/test_chat.py index 2755853e1f7..11ef38dda15 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -21,7 +21,12 @@ import pytest from telegram import ( + Birthdate, Bot, + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, + BusinessOpeningHoursInterval, Chat, ChatLocation, ChatPermissions, @@ -74,6 +79,11 @@ def chat(bot): profile_background_custom_emoji_id=TestChatBase.profile_background_custom_emoji_id, unrestrict_boost_count=TestChatBase.unrestrict_boost_count, custom_emoji_sticker_set_name=TestChatBase.custom_emoji_sticker_set_name, + business_intro=TestChatBase.business_intro, + business_location=TestChatBase.business_location, + business_opening_hours=TestChatBase.business_opening_hours, + birthdate=Birthdate(1, 1), + personal_chat=TestChatBase.personal_chat, ) chat.set_bot(bot) chat._unfreeze() @@ -113,12 +123,20 @@ class TestChatBase: ReactionTypeEmoji(ReactionEmoji.THUMBS_DOWN), ReactionTypeCustomEmoji("custom_emoji_id"), ] + business_intro = BusinessIntro("Title", "Description", None) + business_location = BusinessLocation("Address", Location(123, 456)) + business_opening_hours = BusinessOpeningHours( + "Country/City", + [BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60)], + ) accent_color_id = 1 background_custom_emoji_id = "background_custom_emoji_id" profile_accent_color_id = 2 profile_background_custom_emoji_id = "profile_background_custom_emoji_id" unrestrict_boost_count = 100 custom_emoji_sticker_set_name = "custom_emoji_sticker_set_name" + birthdate = Birthdate(1, 1) + personal_chat = Chat(3, "private", "private") class TestChatWithoutRequest(TestChatBase): @@ -139,6 +157,9 @@ def test_de_json(self, bot): "permissions": self.permissions.to_dict(), "slow_mode_delay": self.slow_mode_delay, "bio": self.bio, + "business_intro": self.business_intro.to_dict(), + "business_location": self.business_location.to_dict(), + "business_opening_hours": self.business_opening_hours.to_dict(), "has_protected_content": self.has_protected_content, "has_visible_history": self.has_visible_history, "has_private_forwards": self.has_private_forwards, @@ -162,6 +183,8 @@ def test_de_json(self, bot): "profile_background_custom_emoji_id": self.profile_background_custom_emoji_id, "unrestrict_boost_count": self.unrestrict_boost_count, "custom_emoji_sticker_set_name": self.custom_emoji_sticker_set_name, + "birthdate": self.birthdate.to_dict(), + "personal_chat": self.personal_chat.to_dict(), } chat = Chat.de_json(json_dict, bot) @@ -174,6 +197,9 @@ def test_de_json(self, bot): assert chat.permissions == self.permissions assert chat.slow_mode_delay == self.slow_mode_delay assert chat.bio == self.bio + assert chat.business_intro == self.business_intro + assert chat.business_location == self.business_location + assert chat.business_opening_hours == self.business_opening_hours assert chat.has_protected_content == self.has_protected_content assert chat.has_visible_history == self.has_visible_history assert chat.has_private_forwards == self.has_private_forwards @@ -202,6 +228,8 @@ def test_de_json(self, bot): assert chat.profile_background_custom_emoji_id == self.profile_background_custom_emoji_id assert chat.unrestrict_boost_count == self.unrestrict_boost_count assert chat.custom_emoji_sticker_set_name == self.custom_emoji_sticker_set_name + assert chat.birthdate == self.birthdate + assert chat.personal_chat == self.personal_chat def test_de_json_localization(self, bot, raw_bot, tz_bot): json_dict = { @@ -234,6 +262,9 @@ def test_to_dict(self, chat): assert chat_dict["permissions"] == chat.permissions.to_dict() assert chat_dict["slow_mode_delay"] == chat.slow_mode_delay assert chat_dict["bio"] == chat.bio + assert chat_dict["business_intro"] == chat.business_intro.to_dict() + assert chat_dict["business_location"] == chat.business_location.to_dict() + assert chat_dict["business_opening_hours"] == chat.business_opening_hours.to_dict() assert chat_dict["has_private_forwards"] == chat.has_private_forwards assert chat_dict["has_protected_content"] == chat.has_protected_content assert chat_dict["has_visible_history"] == chat.has_visible_history @@ -267,6 +298,8 @@ def test_to_dict(self, chat): ) assert chat_dict["custom_emoji_sticker_set_name"] == chat.custom_emoji_sticker_set_name assert chat_dict["unrestrict_boost_count"] == chat.unrestrict_boost_count + assert chat_dict["birthdate"] == chat.birthdate.to_dict() + assert chat_dict["personal_chat"] == chat.personal_chat.to_dict() def test_always_tuples_attributes(self): chat = Chat( @@ -576,6 +609,19 @@ async def make_assertion(*_, **kwargs): custom_title = kwargs["custom_title"] == "custom_title" return chat_id and user_id and custom_title + assert check_shortcut_signature( + Chat.set_administrator_custom_title, + Bot.set_chat_administrator_custom_title, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.set_administrator_custom_title, + chat.get_bot(), + "set_chat_administrator_custom_title", + ) + assert await check_defaults_handling(chat.set_administrator_custom_title, chat.get_bot()) + monkeypatch.setattr("telegram.Bot.set_chat_administrator_custom_title", make_assertion) assert await chat.set_administrator_custom_title(user_id=42, custom_title="custom_title") diff --git a/tests/test_chatadministratorrights.py b/tests/test_chatadministratorrights.py index c7b30be9237..e630693c2d7 100644 --- a/tests/test_chatadministratorrights.py +++ b/tests/test_chatadministratorrights.py @@ -98,38 +98,23 @@ def test_equality(self): a = ChatAdministratorRights( True, *((False,) * 11), - can_post_stories=False, - can_edit_stories=False, - can_delete_stories=False, ) b = ChatAdministratorRights( True, *((False,) * 11), - can_post_stories=False, - can_edit_stories=False, - can_delete_stories=False, ) c = ChatAdministratorRights( *(False,) * 12, - can_post_stories=False, - can_edit_stories=False, - can_delete_stories=False, ) d = ChatAdministratorRights( True, True, *((False,) * 10), - can_post_stories=False, - can_edit_stories=False, - can_delete_stories=False, ) e = ChatAdministratorRights( True, True, *((False,) * 10), - can_post_stories=False, - can_edit_stories=False, - can_delete_stories=False, ) assert a == b @@ -156,9 +141,8 @@ def test_all_rights(self): True, True, True, - can_post_stories=True, - can_edit_stories=True, - can_delete_stories=True, + True, + True, ) t = ChatAdministratorRights.all_rights() # if the dirs are the same, the attributes will all be there @@ -181,9 +165,9 @@ def test_no_rights(self): False, False, False, - can_post_stories=False, - can_edit_stories=False, - can_delete_stories=False, + False, + False, + False, ) t = ChatAdministratorRights.no_rights() # if the dirs are the same, the attributes will all be there @@ -194,19 +178,3 @@ def test_no_rights(self): assert t[key] is False # and as a finisher, make sure the default is different. assert f != t - - def test_depreciation_typeerror(self): - with pytest.raises(TypeError, match="must be set in order"): - ChatAdministratorRights( - *(False,) * 12, - ) - with pytest.raises(TypeError, match="must be set in order"): - ChatAdministratorRights(*(False,) * 12, can_edit_stories=True) - with pytest.raises(TypeError, match="must be set in order"): - ChatAdministratorRights(*(False,) * 12, can_post_stories=True) - with pytest.raises(TypeError, match="must be set in order"): - ChatAdministratorRights(*(False,) * 12, can_delete_stories=True) - with pytest.raises(TypeError, match="must be set in order"): - ChatAdministratorRights(*(False,) * 12, can_edit_stories=True, can_post_stories=True) - with pytest.raises(TypeError, match="must be set in order"): - ChatAdministratorRights(*(False,) * 12, can_delete_stories=True, can_post_stories=True) diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index 90ea90294b7..28293a6cc80 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -89,14 +89,14 @@ def chat_member_administrator(): CMDefaults.can_promote_members, CMDefaults.can_change_info, CMDefaults.can_invite_users, + CMDefaults.can_post_stories, + CMDefaults.can_edit_stories, + CMDefaults.can_delete_stories, CMDefaults.can_post_messages, CMDefaults.can_edit_messages, CMDefaults.can_pin_messages, CMDefaults.can_manage_topics, CMDefaults.custom_title, - CMDefaults.can_post_stories, - CMDefaults.can_edit_stories, - CMDefaults.can_delete_stories, ) @@ -302,19 +302,3 @@ def test_equality(self, chat_member_type): assert c != e assert hash(c) != hash(e) - - def test_deprecation_typeerror(self, chat_member_type): - with pytest.raises(TypeError, match="must be set in order"): - ChatMemberAdministrator( - *(False,) * 12, - ) - with pytest.raises(TypeError, match="must be set in order"): - ChatMemberAdministrator(*(False,) * 12, can_edit_stories=True) - with pytest.raises(TypeError, match="must be set in order"): - ChatMemberAdministrator(*(False,) * 12, can_post_stories=True) - with pytest.raises(TypeError, match="must be set in order"): - ChatMemberAdministrator(*(False,) * 12, can_delete_stories=True) - with pytest.raises(TypeError, match="must be set in order"): - ChatMemberAdministrator(*(False,) * 12, can_edit_stories=True, can_post_stories=True) - with pytest.raises(TypeError, match="must be set in order"): - ChatMemberAdministrator(*(False,) * 12, can_delete_stories=True, can_post_stories=True) diff --git a/tests/test_chatmemberupdated.py b/tests/test_chatmemberupdated.py index 8f9c405c06d..0cf5e58101c 100644 --- a/tests/test_chatmemberupdated.py +++ b/tests/test_chatmemberupdated.py @@ -54,7 +54,6 @@ def old_chat_member(user): def new_chat_member(user): return ChatMemberAdministrator( user, - TestChatMemberUpdatedBase.new_status, True, True, True, @@ -64,9 +63,10 @@ def new_chat_member(user): True, True, True, - can_post_stories=True, - can_edit_stories=True, - can_delete_stories=True, + True, + True, + True, + custom_title=TestChatMemberUpdatedBase.new_status, ) diff --git a/tests/test_constants.py b/tests/test_constants.py index 5055f75795c..98768b806b4 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -149,7 +149,7 @@ def is_type_attribute(name: str) -> bool: if any(re.match(pattern, name) for pattern in patters): return False - if name in { + return name not in { "author_signature", "api_kwargs", "caption", @@ -176,10 +176,8 @@ def is_type_attribute(name: str) -> bool: # attribute is deprecated, no need to add it to MessageType "user_shared", "via_bot", - }: - return False - - return True + "is_from_offline", + } @pytest.mark.parametrize( "attribute", diff --git a/tests/test_message.py b/tests/test_message.py index b98f36b4577..7f04dffbeb1 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -50,6 +50,7 @@ PollOption, ProximityAlertTriggered, ReplyParameters, + SharedUser, Sticker, Story, SuccessfulPayment, @@ -89,6 +90,7 @@ def message(bot): date=TestMessageBase.date, chat=copy(TestMessageBase.chat), from_user=copy(TestMessageBase.from_user), + business_connection_id="123456789", ) message.set_bot(bot) message._unfreeze() @@ -218,7 +220,7 @@ def message(bot): }, {"web_app_data": WebAppData("some_data", "some_button_text")}, {"message_thread_id": 123}, - {"users_shared": UsersShared(1, [2, 3])}, + {"users_shared": UsersShared(1, users=[SharedUser(2, "user2"), SharedUser(3, "user3")])}, {"chat_shared": ChatShared(3, 4)}, { "giveaway": Giveaway( @@ -263,6 +265,9 @@ def message(bot): {"reply_to_story": Story(Chat(1, Chat.PRIVATE), 0)}, {"boost_added": ChatBoostAdded(100)}, {"sender_boost_count": 1}, + {"is_from_offline": True}, + {"sender_business_bot": User(1, "BusinessBot", True)}, + {"business_connection_id": "123456789"}, ], ids=[ "reply", @@ -328,6 +333,9 @@ def message(bot): "reply_to_story", "boost_added", "sender_boost_count", + "sender_business_bot", + "business_connection_id", + "is_from_offline", ], ) def message_params(bot, request): @@ -482,6 +490,48 @@ async def make_assertion(*args, **kwargs): if reply_parameters is None or reply_parameters.message_id != 42: pytest.fail(f"reply_parameters is {reply_parameters} but should be 42") + @staticmethod + async def check_thread_id_parsing( + message: Message, method, bot_method_name: str, args, monkeypatch + ): + """Used in testing reply_* below. Makes sure that meassage_thread_id is parsed + correctly.""" + + async def extract_message_thread_id(*args, **kwargs): + return kwargs.get("message_thread_id") + + monkeypatch.setattr(message.get_bot(), bot_method_name, extract_message_thread_id) + + message.message_thread_id = None + message_thread_id = await method(*args) + assert message_thread_id is None + + message.message_thread_id = 99 + message_thread_id = await method(*args) + assert message_thread_id == 99 + + message_thread_id = await method(*args, message_thread_id=50) + assert message_thread_id == 50 + + if bot_method_name == "send_chat_action": + return + + message_thread_id = await method( + *args, + do_quote=message.build_reply_arguments( + target_chat_id=123, + ), + ) + assert message_thread_id is None + + message_thread_id = await method( + *args, + do_quote=message.build_reply_arguments( + target_chat_id=message.chat_id, + ), + ) + assert message_thread_id == message.message_thread_id + def test_slot_behaviour(self): message = Message( message_id=TestMessageBase.id_, @@ -1344,7 +1394,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_text, Bot.send_message, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1352,6 +1402,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_text, message.get_bot()) @@ -1361,6 +1412,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_text, "send_message", ["test"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_text, "send_message", ["test"], monkeypatch + ) + async def test_reply_markdown(self, monkeypatch, message): test_md_string = ( r"Test for <*bold*, _ita_\__lic_, `code`, " @@ -1378,7 +1433,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_markdown, Bot.send_message, - ["chat_id", "parse_mode", "reply_to_message_id"], + ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1386,6 +1441,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_text, message.get_bot()) @@ -1395,6 +1451,10 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(message.get_bot(), "send_message", make_assertion) assert await message.reply_markdown(self.test_message.text_markdown) + await self.check_thread_id_parsing( + message, message.reply_markdown, "send_message", ["test"], monkeypatch + ) + async def test_reply_markdown_v2(self, monkeypatch, message): test_md_string = ( r"__Test__ for <*bold*, _ita\_lic_, `\\\`code`, " @@ -1416,7 +1476,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_markdown_v2, Bot.send_message, - ["chat_id", "parse_mode", "reply_to_message_id"], + ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1424,6 +1484,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_text, message.get_bot()) @@ -1436,6 +1497,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_markdown_v2, "send_message", [test_md_string], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_markdown_v2, "send_message", ["test"], monkeypatch + ) + async def test_reply_html(self, monkeypatch, message): test_html_string = ( "Test for <bold, ita_lic, " @@ -1459,7 +1524,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_html, Bot.send_message, - ["chat_id", "parse_mode", "reply_to_message_id"], + ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1467,6 +1532,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_text, message.get_bot()) @@ -1479,6 +1545,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_html, "send_message", [test_html_string], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_html, "send_message", ["test"], monkeypatch + ) + async def test_reply_media_group(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1488,7 +1558,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_media_group, Bot.send_media_group, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1496,6 +1566,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_media_group", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_media_group, message.get_bot()) @@ -1509,6 +1580,14 @@ async def make_assertion(*_, **kwargs): monkeypatch, ) + await self.check_thread_id_parsing( + message, + message.reply_media_group, + "send_media_group", + ["reply_media_group"], + monkeypatch, + ) + async def test_reply_photo(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1518,7 +1597,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_photo, Bot.send_photo, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1526,6 +1605,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_photo", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_photo, message.get_bot()) @@ -1535,6 +1615,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_photo, "send_photo", ["test_photo"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_photo, "send_photo", ["test_photo"], monkeypatch + ) + async def test_reply_audio(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1544,7 +1628,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_audio, Bot.send_audio, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1552,6 +1636,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_audio", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_audio, message.get_bot()) @@ -1561,6 +1646,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_audio, "send_audio", ["test_audio"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_audio, "send_audio", ["test_audio"], monkeypatch + ) + async def test_reply_document(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1570,7 +1659,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_document, Bot.send_document, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1578,6 +1667,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_document", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_document, message.get_bot()) @@ -1587,6 +1677,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_document, "send_document", ["test_document"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_document, "send_document", ["test_document"], monkeypatch + ) + async def test_reply_animation(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1596,7 +1690,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_animation, Bot.send_animation, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1604,6 +1698,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_animation", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_animation, message.get_bot()) @@ -1613,6 +1708,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_animation, "send_animation", ["test_animation"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_animation, "send_animation", ["test_animation"], monkeypatch + ) + async def test_reply_sticker(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1622,7 +1721,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_sticker, Bot.send_sticker, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1630,6 +1729,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_sticker", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_sticker, message.get_bot()) @@ -1639,6 +1739,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_sticker, "send_sticker", ["test_sticker"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_sticker, "send_sticker", ["test_sticker"], monkeypatch + ) + async def test_reply_video(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1648,7 +1752,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_video, Bot.send_video, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1656,6 +1760,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_video", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_video, message.get_bot()) @@ -1665,6 +1770,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_video, "send_video", ["test_video"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_video, "send_video", ["test_video"], monkeypatch + ) + async def test_reply_video_note(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1674,7 +1783,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_video_note, Bot.send_video_note, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1682,6 +1791,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_video_note", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_video_note, message.get_bot()) @@ -1691,6 +1801,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_video_note, "send_video_note", ["test_video_note"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_video_note, "send_video_note", ["test_video_note"], monkeypatch + ) + async def test_reply_voice(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1700,7 +1814,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_voice, Bot.send_voice, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1708,6 +1822,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_voice", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_voice, message.get_bot()) @@ -1717,6 +1832,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_voice, "send_voice", ["test_voice"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_voice, "send_voice", ["test_voice"], monkeypatch + ) + async def test_reply_location(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1726,7 +1845,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_location, Bot.send_location, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1734,6 +1853,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_location", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_location, message.get_bot()) @@ -1743,6 +1863,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_location, "send_location", ["test_location"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_location, "send_location", ["test_location"], monkeypatch + ) + async def test_reply_venue(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1752,7 +1876,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_venue, Bot.send_venue, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1760,6 +1884,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_venue", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_venue, message.get_bot()) @@ -1769,6 +1894,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_venue, "send_venue", ["test_venue"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_venue, "send_venue", ["test_venue"], monkeypatch + ) + async def test_reply_contact(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1778,7 +1907,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_contact, Bot.send_contact, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1786,6 +1915,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_contact", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_contact, message.get_bot()) @@ -1795,6 +1925,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_contact, "send_contact", ["test_contact"], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_contact, "send_contact", ["test_contact"], monkeypatch + ) + async def test_reply_poll(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1805,11 +1939,15 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_poll, Bot.send_poll, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( - message.reply_poll, message.get_bot(), "send_poll", skip_params=["reply_to_message_id"] + message.reply_poll, + message.get_bot(), + "send_poll", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_poll, message.get_bot()) @@ -1819,6 +1957,10 @@ async def make_assertion(*_, **kwargs): message, message.reply_poll, "send_poll", ["test_poll", ["1", "2", "3"]], monkeypatch ) + await self.check_thread_id_parsing( + message, message.reply_poll, "send_poll", ["test_poll", ["1", "2", "3"]], monkeypatch + ) + async def test_reply_dice(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1828,11 +1970,15 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_dice, Bot.send_dice, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( - message.reply_dice, message.get_bot(), "send_dice", skip_params=["reply_to_message_id"] + message.reply_dice, + message.get_bot(), + "send_dice", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_dice, message.get_bot()) @@ -1846,6 +1992,10 @@ async def make_assertion(*_, **kwargs): monkeypatch, ) + await self.check_thread_id_parsing( + message, message.reply_dice, "send_dice", [], monkeypatch + ) + async def test_reply_action(self, monkeypatch, message: Message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -1853,16 +2003,30 @@ async def make_assertion(*_, **kwargs): return id_ and action assert check_shortcut_signature( - Message.reply_chat_action, Bot.send_chat_action, ["chat_id", "reply_to_message_id"], [] + Message.reply_chat_action, + Bot.send_chat_action, + ["chat_id", "reply_to_message_id", "business_connection_id"], + [], ) assert await check_shortcut_call( - message.reply_chat_action, message.get_bot(), "send_chat_action" + message.reply_chat_action, + message.get_bot(), + "send_chat_action", + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_chat_action, message.get_bot()) monkeypatch.setattr(message.get_bot(), "send_chat_action", make_assertion) assert await message.reply_chat_action(action=ChatAction.TYPING) + await self.check_thread_id_parsing( + message, + message.reply_chat_action, + "send_chat_action", + [ChatAction.TYPING], + monkeypatch, + ) + async def test_reply_game(self, monkeypatch, message): async def make_assertion(*_, **kwargs): return ( @@ -1872,11 +2036,15 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_game, Bot.send_game, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( - message.reply_game, message.get_bot(), "send_game", skip_params=["reply_to_message_id"] + message.reply_game, + message.get_bot(), + "send_game", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_game, message.get_bot()) @@ -1886,6 +2054,14 @@ async def make_assertion(*_, **kwargs): message, message.reply_game, "send_game", ["test_game"], monkeypatch ) + await self.check_thread_id_parsing( + message, + message.reply_game, + "send_game", + ["test_game"], + monkeypatch, + ) + async def test_reply_invoice(self, monkeypatch, message): async def make_assertion(*_, **kwargs): title = kwargs["title"] == "title" @@ -1900,7 +2076,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_invoice, Bot.send_invoice, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -1908,6 +2084,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_invoice", skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], ) assert await check_defaults_handling(message.reply_invoice, message.get_bot()) @@ -1928,6 +2105,14 @@ async def make_assertion(*_, **kwargs): monkeypatch, ) + await self.check_thread_id_parsing( + message, + message.reply_invoice, + "send_invoice", + ["title", "description", "payload", "provider_token", "currency", "prices"], + monkeypatch, + ) + @pytest.mark.parametrize(("disable_notification", "protected"), [(False, True), (True, False)]) async def test_forward(self, monkeypatch, message, disable_notification, protected): async def make_assertion(*_, **kwargs): @@ -2017,7 +2202,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_copy, Bot.copy_message, - ["chat_id", "reply_to_message_id"], + ["chat_id", "reply_to_message_id", "business_connection_id"], ["quote", "do_quote", "reply_to_message_id"], ) assert await check_shortcut_call(message.copy, message.get_bot(), "copy_message") @@ -2042,6 +2227,14 @@ async def make_assertion(*_, **kwargs): monkeypatch, ) + await self.check_thread_id_parsing( + message, + message.reply_copy, + "copy_message", + [123456, 456789], + monkeypatch, + ) + async def test_edit_text(self, monkeypatch, message): async def make_assertion(*_, **kwargs): chat_id = kwargs["chat_id"] == message.chat_id diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index edb1ed2ab12..89892741bd4 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -132,9 +132,6 @@ def ptb_extra_params(object_name: str) -> set[str]: # Mostly due to the value being fixed anyway PTB_IGNORED_PARAMS = { r"InlineQueryResult\w+": {"type"}, - # TODO: Remove this in v21.0 (API 7.1) when this can stop being optional - r"ChatAdministratorRights": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, - r"ChatMemberAdministrator": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, r"ChatMember\w+": {"status"}, r"PassportElementError\w+": {"source"}, "ForceReply": {"force_reply"}, @@ -170,9 +167,9 @@ 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]] = { - # TODO: Remove this in v21.0 (API 7.1) when this can stop being optional - r"ChatAdministratorRights": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, - r"ChatMemberAdministrator": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, + "create_new_sticker_set": {"sticker_format"}, # removed by bot api 7.2 + "StickerSet": {"is_animated", "is_video"}, # removed by bot api 7.2 + "UsersShared": {"user_ids", "users"}, # removed/added by bot api 7.2 } diff --git a/tests/test_shared.py b/tests/test_shared.py index 3c76eb329f5..fcad7ec345a 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -19,19 +19,20 @@ import pytest -from telegram import ChatShared, UsersShared +from telegram import ChatShared, PhotoSize, SharedUser, UsersShared +from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @pytest.fixture(scope="class") def users_shared(): - return UsersShared(TestUsersSharedBase.request_id, TestUsersSharedBase.user_ids) + return UsersShared(TestUsersSharedBase.request_id, users=TestUsersSharedBase.users) class TestUsersSharedBase: request_id = 789 - user_id = 101112 - user_ids = (user_id, 101113) + user_ids = (101112, 101113) + users = (SharedUser(101112, "user1"), SharedUser(101113, "user2")) class TestUsersSharedWithoutRequest(TestUsersSharedBase): @@ -45,24 +46,43 @@ def test_to_dict(self, users_shared): assert isinstance(users_shared_dict, dict) assert users_shared_dict["request_id"] == self.request_id - assert users_shared_dict["user_ids"] == list(self.user_ids) + assert users_shared_dict["users"] == [user.to_dict() for user in self.users] def test_de_json(self, bot): json_dict = { "request_id": self.request_id, + "users": [user.to_dict() for user in self.users], "user_ids": self.user_ids, } users_shared = UsersShared.de_json(json_dict, bot) - assert users_shared.api_kwargs == {} + assert users_shared.api_kwargs == {"user_ids": self.user_ids} assert users_shared.request_id == self.request_id + assert users_shared.users == self.users assert users_shared.user_ids == tuple(self.user_ids) + assert UsersShared.de_json({}, bot) is None + + def test_users_is_required_argument(self): + with pytest.raises(TypeError, match="`users` is a required argument"): + UsersShared(self.request_id, user_ids=self.user_ids) + + def test_user_ids_deprecation_warning(self): + with pytest.warns( + PTBDeprecationWarning, match="'user_ids' was renamed to 'users' in Bot API 7.2" + ): + users_shared = UsersShared(self.request_id, user_ids=self.user_ids, users=self.users) + with pytest.warns( + PTBDeprecationWarning, match="renamed the attribute 'user_ids' to 'users'" + ): + users_shared.user_ids + def test_equality(self): - a = UsersShared(self.request_id, self.user_ids) - b = UsersShared(self.request_id, self.user_ids) - c = UsersShared(1, self.user_ids) - d = UsersShared(self.request_id, [1, 2]) + a = UsersShared(self.request_id, users=self.users) + b = UsersShared(self.request_id, users=self.users) + c = UsersShared(1, users=self.users) + d = UsersShared(self.request_id, users=(SharedUser(1, "user1"), SharedUser(1, "user2"))) + e = PhotoSize("file_id", "1", 1, 1) assert a == b assert hash(a) == hash(b) @@ -74,6 +94,9 @@ def test_equality(self): assert a != d assert hash(a) != hash(d) + assert a != e + assert hash(a) != hash(e) + @pytest.fixture(scope="class") def chat_shared(): @@ -112,11 +135,109 @@ def test_de_json(self, bot): assert chat_shared.request_id == self.request_id assert chat_shared.chat_id == self.chat_id - def test_equality(self): + def test_equality(self, users_shared): a = ChatShared(self.request_id, self.chat_id) b = ChatShared(self.request_id, self.chat_id) c = ChatShared(1, self.chat_id) d = ChatShared(self.request_id, 1) + e = users_shared + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="class") +def shared_user(): + return SharedUser( + TestSharedUserBase.user_id, + TestSharedUserBase.first_name, + last_name=TestSharedUserBase.last_name, + username=TestSharedUserBase.username, + photo=TestSharedUserBase.photo, + ) + + +class TestSharedUserBase: + user_id = 101112 + first_name = "first" + last_name = "last" + username = "user" + photo = ( + PhotoSize(file_id="file_id", width=1, height=1, file_unique_id="1"), + PhotoSize(file_id="file_id", width=2, height=2, file_unique_id="2"), + ) + + +class TestSharedUserWithoutRequest(TestSharedUserBase): + def test_slot_behaviour(self, shared_user): + for attr in shared_user.__slots__: + assert getattr(shared_user, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(shared_user)) == len(set(mro_slots(shared_user))), "duplicate slot" + + def test_to_dict(self, shared_user): + shared_user_dict = shared_user.to_dict() + + assert isinstance(shared_user_dict, dict) + assert shared_user_dict["user_id"] == self.user_id + assert shared_user_dict["first_name"] == self.first_name + assert shared_user_dict["last_name"] == self.last_name + assert shared_user_dict["username"] == self.username + assert shared_user_dict["photo"] == [photo.to_dict() for photo in self.photo] + + def test_de_json_required(self, bot): + json_dict = { + "user_id": self.user_id, + "first_name": self.first_name, + } + shared_user = SharedUser.de_json(json_dict, bot) + assert shared_user.api_kwargs == {} + + assert shared_user.user_id == self.user_id + assert shared_user.first_name == self.first_name + assert shared_user.last_name is None + assert shared_user.username is None + assert shared_user.photo == () + + def test_de_json_all(self, bot): + json_dict = { + "user_id": self.user_id, + "first_name": self.first_name, + "last_name": self.last_name, + "username": self.username, + "photo": [photo.to_dict() for photo in self.photo], + } + shared_user = SharedUser.de_json(json_dict, bot) + assert shared_user.api_kwargs == {} + + assert shared_user.user_id == self.user_id + assert shared_user.first_name == self.first_name + assert shared_user.last_name == self.last_name + assert shared_user.username == self.username + assert shared_user.photo == self.photo + + assert SharedUser.de_json({}, bot) is None + + def test_equality(self, chat_shared): + a = SharedUser( + self.user_id, + self.first_name, + last_name=self.last_name, + username=self.username, + photo=self.photo, + ) + b = SharedUser(self.user_id, "other_firs_name") + c = SharedUser(self.user_id + 1, self.first_name) + d = chat_shared assert a == b assert hash(a) == hash(b) diff --git a/tests/test_update.py b/tests/test_update.py index 7c7a35a4c02..b314c98e819 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -17,11 +17,14 @@ # 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 time +from copy import deepcopy from datetime import datetime import pytest from telegram import ( + BusinessConnection, + BusinessMessagesDeleted, CallbackQuery, Chat, ChatBoost, @@ -51,7 +54,21 @@ from telegram.warnings import PTBUserWarning from tests.auxil.slots import mro_slots -message = Message(1, datetime.utcnow(), Chat(1, ""), from_user=User(1, "", False), text="Text") +message = Message( + 1, + datetime.utcnow(), + Chat(1, ""), + from_user=User(1, "", False), + text="Text", + sender_chat=Chat(1, ""), +) +channel_post = Message( + 1, + datetime.utcnow(), + Chat(1, ""), + text="Text", + sender_chat=Chat(1, ""), +) chat_member_updated = ChatMemberUpdated( Chat(1, "chat"), User(1, "", False), @@ -93,6 +110,7 @@ old_reaction=(ReactionTypeEmoji("👍"),), new_reaction=(ReactionTypeEmoji("👍"),), user=User(1, "name", False), + actor_chat=Chat(1, ""), ) @@ -103,13 +121,35 @@ 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}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, - {"channel_post": message}, - {"edited_channel_post": message}, + {"channel_post": channel_post}, + {"edited_channel_post": channel_post}, {"inline_query": InlineQuery(1, User(1, "", False), "", "")}, {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, @@ -134,6 +174,10 @@ {"removed_chat_boost": removed_chat_boost}, {"message_reaction": message_reaction}, {"message_reaction_count": message_reaction_count}, + {"business_connection": business_connection}, + {"deleted_business_messages": deleted_business_messages}, + {"business_message": business_message}, + {"edited_business_message": business_message}, # Must be last to conform with `ids` below! {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, ] @@ -157,6 +201,10 @@ "removed_chat_boost", "message_reaction", "message_reaction_count", + "business_connection", + "deleted_business_messages", + "business_message", + "edited_business_message", ) ids = (*all_types, "callback_query_without_message") @@ -241,6 +289,7 @@ def test_effective_chat(self, update): or update.pre_checkout_query is not None or update.poll is not None or update.poll_answer is not None + or update.business_connection is not None ): assert chat.id == 1 else: @@ -256,11 +305,84 @@ def test_effective_user(self, update): or update.chat_boost is not None or update.removed_chat_boost is not None or update.message_reaction_count is not None + or update.deleted_business_messages is not None ): assert user.id == 1 else: assert user is None + def test_effective_sender_non_anonymous(self, update): + update = deepcopy(update) + # Simulate 'Remain anonymous' being turned off + if message := (update.message or update.edited_message): + message._unfreeze() + message.sender_chat = None + elif reaction := (update.message_reaction): + reaction._unfreeze() + reaction.actor_chat = None + elif answer := (update.poll_answer): + answer._unfreeze() + answer.voter_chat = None + + # Test that it's sometimes None per docstring + sender = update.effective_sender + if not ( + update.poll is not None + 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) + else: + assert isinstance(sender, User) + + else: + assert sender is None + + cached = update.effective_sender + assert cached is sender + + def test_effective_sender_anonymous(self, update): + update = deepcopy(update) + # Simulate 'Remain anonymous' being turned on + if message := (update.message or update.edited_message): + message._unfreeze() + message.from_user = None + elif reaction := (update.message_reaction): + reaction._unfreeze() + reaction.user = None + elif answer := (update.poll_answer): + answer._unfreeze() + answer.user = None + + # Test that it's sometimes None per docstring + sender = update.effective_sender + if not ( + update.poll is not None + 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 + or update.edited_message + or update.channel_post + or update.edited_channel_post + or update.message_reaction + or update.poll_answer + ): + assert isinstance(sender, Chat) + else: + assert isinstance(sender, User) + else: + assert sender is None + + cached = update.effective_sender + assert cached is sender + def test_effective_message(self, update): # Test that it's sometimes None per docstring eff_message = update.effective_message @@ -279,6 +401,8 @@ def test_effective_message(self, update): or update.removed_chat_boost is not None or update.message_reaction is not None or update.message_reaction_count is not None + or update.deleted_business_messages is not None + or update.business_connection is not None ): assert eff_message.message_id == message.message_id else: diff --git a/tests/test_user.py b/tests/test_user.py index 9edc0db6b53..86faa73cd77 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -42,6 +42,7 @@ def json_dict(): "supports_inline_queries": TestUserBase.supports_inline_queries, "is_premium": TestUserBase.is_premium, "added_to_attachment_menu": TestUserBase.added_to_attachment_menu, + "can_connect_to_business": TestUserBase.can_connect_to_business, } @@ -59,6 +60,7 @@ def user(bot): supports_inline_queries=TestUserBase.supports_inline_queries, is_premium=TestUserBase.is_premium, added_to_attachment_menu=TestUserBase.added_to_attachment_menu, + can_connect_to_business=TestUserBase.can_connect_to_business, ) user.set_bot(bot) user._unfreeze() @@ -77,6 +79,7 @@ class TestUserBase: supports_inline_queries = False is_premium = True added_to_attachment_menu = False + can_connect_to_business = True class TestUserWithoutRequest(TestUserBase): @@ -100,6 +103,7 @@ def test_de_json(self, json_dict, bot): assert user.supports_inline_queries == self.supports_inline_queries assert user.is_premium == self.is_premium assert user.added_to_attachment_menu == self.added_to_attachment_menu + assert user.can_connect_to_business == self.can_connect_to_business def test_to_dict(self, user): user_dict = user.to_dict() @@ -116,6 +120,7 @@ def test_to_dict(self, user): assert user_dict["supports_inline_queries"] == user.supports_inline_queries assert user_dict["is_premium"] == user.is_premium assert user_dict["added_to_attachment_menu"] == user.added_to_attachment_menu + assert user_dict["can_connect_to_business"] == user.can_connect_to_business def test_equality(self): a = User(self.id_, self.first_name, self.is_bot, self.last_name)