From 9e685fc0c52f20331b4fcbfdc126c8e736f14ee3 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 14 Apr 2025 21:37:43 +0200 Subject: [PATCH 1/8] Add several business methods --- docs/source/inclusions/bot_methods.rst | 30 ++- telegram/_bot.py | 245 +++++++++++++++++- telegram/constants.py | 35 +++ telegram/ext/_extbot.py | 121 ++++++++- tests/test_bot.py | 21 -- ...t_business.py => test_business_classes.py} | 0 tests/test_business_methods.py | 120 +++++++++ 7 files changed, 545 insertions(+), 27 deletions(-) rename tests/{test_business.py => test_business_classes.py} (100%) create mode 100644 tests/test_business_methods.py diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 240c258f68f..3123490dd63 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -161,8 +161,6 @@ - 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` @@ -397,6 +395,34 @@ * - :meth:`~telegram.Bot.refund_star_payment` - Used for refunding a payment in Telegram Stars +.. raw:: html + + +
+ +.. raw:: html + +
+ Business Related Methods + +.. list-table:: + :align: left + :widths: 1 4 + + * - :meth:`~telegram.Bot.get_business_connection` + - Used for getting information about the business account. + * - :meth:`~telegram.Bot.read_business_message` + - Used for marking a message as read. + * - :meth:`~telegram.Bot.delete_business_messages` + - Used for deleting business messages. + * - :meth:`~telegram.Bot.set_business_account_name` + - Used for setting the business account name. + * - :meth:`~telegram.Bot.set_business_account_username` + - Used for setting the business account username. + * - :meth:`~telegram.Bot.set_business_account_bio` + - Used for setting the business account bio. + + .. raw:: html
diff --git a/telegram/_bot.py b/telegram/_bot.py index 56a0d08a538..a53bfe8737e 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -48,6 +48,8 @@ serialization = None # type: ignore[assignment] CRYPTO_INSTALLED = False +from typing_extensions import Self + from telegram._botcommand import BotCommand from telegram._botcommandscope import BotCommandScope from telegram._botdescription import BotDescription, BotShortDescription @@ -380,7 +382,7 @@ def __init__( self._freeze() - async def __aenter__(self: BT) -> BT: + async def __aenter__(self) -> Self: """ |async_context_manager| :meth:`initializes ` the Bot. @@ -3928,7 +3930,7 @@ async def get_file( api_kwargs=api_kwargs, ) - file_path = cast(dict, result).get("file_path") + file_path = cast("dict", result).get("file_path") if file_path and not is_local_file(file_path): result["file_path"] = f"{self._base_file_url}/{file_path}" @@ -4591,7 +4593,7 @@ async def get_updates( # waiting for the server to return and there's no way of knowing the connection had been # dropped in real time. result = cast( - list[JSONDict], + "list[JSONDict]", await self._post( "getUpdates", data, @@ -9401,6 +9403,233 @@ async def get_business_connection( bot=self, ) + async def read_business_message( + self, + business_connection_id: str, + chat_id: int, + message_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """ + Marks incoming message as read on behalf of a business account. + Requires the ``can_read_messages`` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection on + behalf of which to read the message. + chat_id (:obj:`int`): Unique identifier of the chat in which the message was received. + The chat must have been active in the last + :tg-const:`~telegram.constants.BusinessLimit.\ +READ_BUSINESS_MESSAGE_ACTIVITY_TIMEOUT` seconds. + message_id (:obj:`int`): Unique identifier of the message to mark as read. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "chat_id": chat_id, + "message_id": message_id, + } + return await self._post( + "readBusinessMessage", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_business_messages( + self, + business_connection_id: str, + message_ids: Sequence[int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """ + Delete messages on behalf of a business account. Requires the + ``can_delete_outgoing_messages`` business bot right to delete messages sent by the bot + itself, or the ``can_delete_all_messages`` business bot right to delete any message. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`int` | :obj:`str`): Unique identifier of the business + connection on behalf of which to delete the messages + message_ids (Sequence[:obj:`int`]): A list of + :tg-const:`telegram.constants.BulkRequestLimit.MIN_LIMIT`- + :tg-const:`telegram.constants.BulkRequestLimit.MAX_LIMIT` identifiers of messages + to delete. See :meth:`delete_message` for limitations on which messages can be + deleted. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "message_ids": parse_sequence_arg(message_ids), + } + return await self._post( + "deleteBusinessMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_business_account_name( + self, + business_connection_id: str, + first_name: str, + last_name: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """ + Changes the first and last name of a managed business account. Requires the + ``can_change_name`` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`int` | :obj:`str`): Unique identifier of the business + connection + first_name (:obj:`str`): New first name of the business account; + :tg-const:`telegram.constants.BusinessLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.BusinessLimit.MAX_NAME_LENGTH` characters. + last_name (:obj:`str`, optional): New last name of the business account; + 0-:tg-const:`telegram.constants.BusinessLimit.MAX_NAME_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "first_name": first_name, + "last_name": last_name, + } + return await self._post( + "setBusinessAccountName", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_business_account_username( + self, + business_connection_id: str, + username: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """ + Changes the username of a managed business account. Requires the + ``can_change_username`` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + username (:obj:`str`, optional): New business account username; + 0-:tg-const:`telegram.constants.BusinessLimit.MAX_USERNAME_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "username": username, + } + return await self._post( + "setBusinessAccountUsername", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_business_account_bio( + self, + business_connection_id: str, + bio: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """ + Changes the bio of a managed business account. Requires the ``can_change_bio`` business + bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + bio (:obj:`str`, optional): The new value of the bio for the business account; + 0-:tg-const:`telegram.constants.BusinessLimit.MAX_BIO_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "bio": bio, + } + return await self._post( + "setBusinessAccountBio", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def replace_sticker_in_set( self, user_id: int, @@ -10338,6 +10567,16 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`set_message_reaction`""" getBusinessConnection = get_business_connection """Alias for :meth:`get_business_connection`""" + readBusinessMessage = read_business_message + """Alias for :meth:`read_business_message`""" + deleteBusinessMessages = delete_business_messages + """Alias for :meth:`delete_business_messages`""" + setBusinessAccountName = set_business_account_name + """Alias for :meth:`set_business_account_name`""" + setBusinessAccountUsername = set_business_account_username + """Alias for :meth:`set_business_account_username`""" + setBusinessAccountBio = set_business_account_bio + """Alias for :meth:`set_business_account_bio`""" replaceStickerInSet = replace_sticker_in_set """Alias for :meth:`replace_sticker_in_set`""" refundStarPayment = refund_star_payment diff --git a/telegram/constants.py b/telegram/constants.py index ca8e34aea46..a2b15c41efd 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -46,6 +46,7 @@ "BotDescriptionLimit", "BotNameLimit", "BulkRequestLimit", + "BusinessLimit", "CallbackQueryLimit", "ChatAction", "ChatBoostSources", @@ -702,6 +703,40 @@ class BulkRequestLimit(IntEnum): """:obj:`int`: Maximum number of messages required for bulk actions.""" +class BusinessLimit(IntEnum): + """This enum contains limitations related to handling business accounts. The enum members + of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + READ_BUSINESS_MESSAGE_ACTIVITY_TIMEOUT = dtm.timedelta(hours=24).total_seconds() + """:obj:`int`: Time in seconds in which the chat must have been active for + :meth:`~telegram.Bot.read_business_message` to work. + """ + MIN_NAME_LENGTH = 1 + """:obj:`int`: Minimum length of the name of a business account. Relevant only for + :paramref:`~telegram.Bot.set_business_account_name.first_name` of + :meth:`telegram.Bot.set_business_account_name`. + """ + MAX_NAME_LENGTH = 64 + """:obj:`int`: Maximum length of the name of a business account. Relevant for the parameters + of :meth:`telegram.Bot.set_business_account_name`. + """ + MAX_USERNAME_LENGTH = 32 + """::obj:`int`: Maximum length of the username of a business account. Relevant for + :paramref:`~telegram.Bot.set_business_account_username.username` of + :meth:`telegram.Bot.set_business_account_username`. + """ + MAX_BIO_LENGTH = 140 + """:obj:`int`: Maximum length of the bio of a business account. Relevant for + :paramref:`~telegram.Bot.set_business_account_bio.bio` of + :meth:`telegram.Bot.set_business_account_bio`. + """ + + class CallbackQueryLimit(IntEnum): """This enum contains limitations for :class:`telegram.CallbackQuery`/ :meth:`telegram.Bot.answer_callback_query`. The enum members of this enumeration are instances diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 20a72917074..a3b437b3fa1 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -253,7 +253,7 @@ def __init__( return if not isinstance(arbitrary_callback_data, bool): - maxsize = cast(int, arbitrary_callback_data) + maxsize = cast("int", arbitrary_callback_data) else: maxsize = 1024 @@ -4262,6 +4262,120 @@ async def get_business_connection( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def read_business_message( + self, + business_connection_id: str, + chat_id: int, + message_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> bool: + return await super().read_business_message( + business_connection_id=business_connection_id, + chat_id=chat_id, + message_id=message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def delete_business_messages( + self, + business_connection_id: str, + message_ids: Sequence[int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> bool: + return await super().delete_business_messages( + business_connection_id=business_connection_id, + message_ids=message_ids, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def set_business_account_name( + self, + business_connection_id: str, + first_name: str, + last_name: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> bool: + return await super().set_business_account_name( + business_connection_id=business_connection_id, + first_name=first_name, + last_name=last_name, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def set_business_account_username( + self, + business_connection_id: str, + username: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> bool: + return await super().set_business_account_username( + business_connection_id=business_connection_id, + username=username, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def set_business_account_bio( + self, + business_connection_id: str, + bio: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> bool: + return await super().set_business_account_bio( + business_connection_id=business_connection_id, + bio=bio, + 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, @@ -4712,6 +4826,11 @@ async def remove_user_verification( getUserChatBoosts = get_user_chat_boosts setMessageReaction = set_message_reaction getBusinessConnection = get_business_connection + readBusinessMessage = read_business_message + deleteBusinessMessages = delete_business_messages + setBusinessAccountName = set_business_account_name + setBusinessAccountUsername = set_business_account_username + setBusinessAccountBio = set_business_account_bio replaceStickerInSet = replace_sticker_in_set refundStarPayment = refund_star_payment getStarTransactions = get_star_transactions diff --git a/tests/test_bot.py b/tests/test_bot.py index 2f8f2431282..47748fd48a3 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -38,7 +38,6 @@ BotDescription, BotName, BotShortDescription, - BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -2373,26 +2372,6 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): monkeypatch.setattr(offline_bot.request, "post", make_assertion) assert await offline_bot.send_message(2, "text", allow_paid_broadcast=42) - async def test_get_business_connection(self, offline_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(offline_bot.request, "do_request", do_request) - obj = await offline_bot.get_business_connection(business_connection_id=bci) - assert isinstance(obj, BusinessConnection) - async def test_send_chat_action_all_args(self, bot, chat_id, monkeypatch): async def make_assertion(*args, **_): kwargs = args[1] diff --git a/tests/test_business.py b/tests/test_business_classes.py similarity index 100% rename from tests/test_business.py rename to tests/test_business_classes.py diff --git a/tests/test_business_methods.py b/tests/test_business_methods.py new file mode 100644 index 00000000000..1617553bba3 --- /dev/null +++ b/tests/test_business_methods.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 datetime as dtm + +import pytest + +from telegram import BusinessConnection, User + + +class BusinessMethodsTestBase: + bci = "42" + + +class TestBusinessMethodsWithoutRequest(BusinessMethodsTestBase): + async def test_get_business_connection(self, offline_bot, monkeypatch): + user = User(1, "first", False) + user_chat_id = 1 + date = dtm.datetime.utcnow() + can_reply = True + is_enabled = True + bc = BusinessConnection( + self.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 == self.bci: + return 200, f'{{"ok": true, "result": {bc}}}'.encode() + return 400, b'{"ok": false, "result": []}' + + monkeypatch.setattr(offline_bot.request, "do_request", do_request) + obj = await offline_bot.get_business_connection(business_connection_id=self.bci) + assert isinstance(obj, BusinessConnection) + + async def test_read_business_message(self, offline_bot, monkeypatch): + chat_id = 43 + message_id = 44 + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("chat_id") == chat_id + assert data.get("message_id") == message_id + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.read_business_message( + business_connection_id=self.bci, chat_id=chat_id, message_id=message_id + ) + + async def test_delete_business_messages(self, offline_bot, monkeypatch): + message_ids = [1, 2, 3] + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("message_ids") == message_ids + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.delete_business_messages( + business_connection_id=self.bci, message_ids=message_ids + ) + + @pytest.mark.parametrize("last_name", [None, "last_name"]) + async def test_set_business_account_name(self, offline_bot, monkeypatch, last_name): + first_name = "Test Business Account" + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("first_name") == first_name + assert data.get("last_name") == last_name + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.set_business_account_name( + business_connection_id=self.bci, first_name=first_name, last_name=last_name + ) + + @pytest.mark.parametrize("username", ["username", None]) + async def test_set_business_account_username(self, offline_bot, monkeypatch, username): + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("username") == username + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.set_business_account_username( + business_connection_id=self.bci, username=username + ) + + @pytest.mark.parametrize("bio", ["bio", None]) + async def test_set_business_account_bio(self, offline_bot, monkeypatch, bio): + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("bio") == bio + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.set_business_account_bio(business_connection_id=self.bci, bio=bio) From 5faffc96c90ac6cdd0919311b44de84937b567ae Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 14 Apr 2025 21:51:04 +0200 Subject: [PATCH 2/8] Add shortcut methods --- telegram/_chat.py | 34 ++++++++++++++++++++++++++++++++++ telegram/_message.py | 37 +++++++++++++++++++++++++++++++++++++ tests/test_chat.py | 16 ++++++++++++++++ tests/test_message.py | 25 +++++++++++++++++++++++++ 4 files changed, 112 insertions(+) diff --git a/telegram/_chat.py b/telegram/_chat.py index fe49dc3593e..281940f467a 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -3568,6 +3568,40 @@ async def remove_verification( api_kwargs=api_kwargs, ) + async def read_business_message( + self, + business_connection_id: str, + message_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """Shortcut for:: + + await bot.read_business_message(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.read_business_message`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().read_business_message( + chat_id=self.id, + business_connection_id=business_connection_id, + message_id=message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + class Chat(_ChatBase): """This object represents a chat. diff --git a/telegram/_message.py b/telegram/_message.py index 646266be84f..8871145cf11 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -4479,6 +4479,43 @@ async def set_reaction( api_kwargs=api_kwargs, ) + async def read_business_message( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """Shortcut for:: + + await bot.read_business_message( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.read_business_message`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + """ + return await self.get_bot().read_business_message( + chat_id=self.chat_id, + message_id=self.message_id, + business_connection_id=self.business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def parse_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. diff --git a/tests/test_chat.py b/tests/test_chat.py index f53a0fdd2fe..0f1dfeaa072 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1384,6 +1384,22 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "remove_chat_verification", make_assertion) assert await chat.remove_verification() + async def test_instance_method_read_business_message(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["business_connection_id"] == "business_connection_id" + and kwargs["message_id"] == "message_id" + ) + + assert check_shortcut_signature( + Chat.read_business_message, Bot.read_business_message, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.read_business_message, chat.get_bot(), "read_business_message" + ) + assert await check_defaults_handling(chat.read_business_message, chat.get_bot()) + def test_mention_html(self): chat = Chat(id=1, type="foo") with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): diff --git a/tests/test_message.py b/tests/test_message.py index 7150a0502a1..c713cd38862 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -2820,6 +2820,31 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(message.get_bot(), "unpin_all_forum_topic_messages", make_assertion) assert await message.unpin_all_forum_topic_messages() + async def test_read_business_message(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["business_connection_id"] == message.business_connection_id + and kwargs["message_id"] == message.message_id, + ) + + assert check_shortcut_signature( + Message.read_business_message, + Bot.read_business_message, + ["chat_id", "message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.read_business_message, + message.get_bot(), + "read_business_message", + shortcut_kwargs=["chat_id", "message_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.read_business_message, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "read_business_message", make_assertion) + assert await message.read_business_message() + def test_attachement_successful_payment_deprecated(self, message, recwarn): message.successful_payment = "something" # kinda unnecessary to assert but one needs to call the function ofc so. Here we are. From dcf4c0bebf4c437c4dc09aadf9c21866109b4dd7 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:55:29 +0000 Subject: [PATCH 3/8] Add chango fragment for PR #4757 --- changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml diff --git a/changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml b/changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml new file mode 100644 index 00000000000..83801dce428 --- /dev/null +++ b/changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml @@ -0,0 +1,5 @@ +features = "Api 9.0 business methods" +[[pull_requests]] +uid = "4757" +author_uid = "Bibo-Joshi" +closes_threads = [] From d067b05e596d2c3592e88d7dbe4338da25c07d5c Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 14 Apr 2025 21:54:49 +0200 Subject: [PATCH 4/8] revert some accidental ruff changes --- telegram/_bot.py | 8 +++----- telegram/ext/_extbot.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index a53bfe8737e..e8a06bcc32a 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -48,8 +48,6 @@ serialization = None # type: ignore[assignment] CRYPTO_INSTALLED = False -from typing_extensions import Self - from telegram._botcommand import BotCommand from telegram._botcommandscope import BotCommandScope from telegram._botdescription import BotDescription, BotShortDescription @@ -382,7 +380,7 @@ def __init__( self._freeze() - async def __aenter__(self) -> Self: + async def __aenter__(self: BT) -> BT: """ |async_context_manager| :meth:`initializes ` the Bot. @@ -3930,7 +3928,7 @@ async def get_file( api_kwargs=api_kwargs, ) - file_path = cast("dict", result).get("file_path") + file_path = cast(dict, result).get("file_path") if file_path and not is_local_file(file_path): result["file_path"] = f"{self._base_file_url}/{file_path}" @@ -4593,7 +4591,7 @@ async def get_updates( # waiting for the server to return and there's no way of knowing the connection had been # dropped in real time. result = cast( - "list[JSONDict]", + list[JSONDict], await self._post( "getUpdates", data, diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index a3b437b3fa1..49308bc1534 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -253,7 +253,7 @@ def __init__( return if not isinstance(arbitrary_callback_data, bool): - maxsize = cast("int", arbitrary_callback_data) + maxsize = cast(int, arbitrary_callback_data) else: maxsize = 1024 From bb82a546b8cc9bb7cdaaa5121a033cf745d8cd9f Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 14 Apr 2025 21:57:52 +0200 Subject: [PATCH 5/8] update chango fragments --- changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml | 4 ++++ changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml diff --git a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml index d7da32f643c..219c3412640 100644 --- a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml +++ b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml @@ -3,3 +3,7 @@ features = "Full Support for Bot API 9.0" uid = "4756" author_uid = "Bibo-Joshi" closes_threads = ["4754"] +[[pull_requests]] +uid = "4757" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml b/changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml deleted file mode 100644 index 83801dce428..00000000000 --- a/changes/unreleased/4757.GsMGs86c9aGvTR75rnBYHV.toml +++ /dev/null @@ -1,5 +0,0 @@ -features = "Api 9.0 business methods" -[[pull_requests]] -uid = "4757" -author_uid = "Bibo-Joshi" -closes_threads = [] From 69bd6eb4f94177f205e4952b54220ae563a2885f Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 14 Apr 2025 22:05:38 +0200 Subject: [PATCH 6/8] Fix tests --- telegram/_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index e8a06bcc32a..6e706ce0953 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9484,7 +9484,7 @@ async def delete_business_messages( """ data: JSONDict = { "business_connection_id": business_connection_id, - "message_ids": parse_sequence_arg(message_ids), + "message_ids": message_ids, } return await self._post( "deleteBusinessMessages", From 55c28b59d366563651abf830491f7521e5dba82b Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 16 Apr 2025 16:30:21 +0200 Subject: [PATCH 7/8] explicitly cast time Delta to seconds Co-authored-by: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> --- telegram/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/constants.py b/telegram/constants.py index a2b15c41efd..b7e98462fac 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -712,7 +712,7 @@ class BusinessLimit(IntEnum): __slots__ = () - READ_BUSINESS_MESSAGE_ACTIVITY_TIMEOUT = dtm.timedelta(hours=24).total_seconds() + READ_BUSINESS_MESSAGE_ACTIVITY_TIMEOUT = int(dtm.timedelta(hours=24).total_seconds()) """:obj:`int`: Time in seconds in which the chat must have been active for :meth:`~telegram.Bot.read_business_message` to work. """ From eb108975f1534bf1504c6b9ba09ad7e5a1b2a35f Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:30:19 +0200 Subject: [PATCH 8/8] add missing assertion in `test_instance_method_read_business_message` --- tests/test_chat.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_chat.py b/tests/test_chat.py index 0f1dfeaa072..c241b080392 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1400,6 +1400,11 @@ async def make_assertion(*_, **kwargs): ) assert await check_defaults_handling(chat.read_business_message, chat.get_bot()) + monkeypatch.setattr(chat.get_bot(), "read_business_message", make_assertion) + assert await chat.read_business_message( + message_id="message_id", business_connection_id="business_connection_id" + ) + def test_mention_html(self): chat = Chat(id=1, type="foo") with pytest.raises(TypeError, match="Can not create a mention to a private group chat"):