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"):