From 170efd274fa38e3276e3d4382ea03cc034dbd543 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 17 Aug 2025 00:04:04 +0300 Subject: [PATCH 01/17] Bump Bot API Version --- README.rst | 4 ++-- src/telegram/constants.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9d0ff953ba7..4a44f9a7986 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-9.1-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-9.2-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with Telegram API support ~~~~~~~~~~~~~~~~~~~~ -All types and methods of the Telegram Bot API **9.1** are natively supported by this library. +All types and methods of the Telegram Bot API **9.2** are natively supported by this library. In addition, Bot API functionality not yet natively included can still be used as described `in our wiki `_. Notable Features diff --git a/src/telegram/constants.py b/src/telegram/constants.py index a403a78e0cd..435f1d8684c 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -173,7 +173,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=1) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=2) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. From 95cb2b570d00acfcb86227a1087b9daaa76b3618 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sat, 16 Aug 2025 21:17:26 +0000 Subject: [PATCH 02/17] Add chango fragment for PR #4911 --- changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml new file mode 100644 index 00000000000..982353ce67a --- /dev/null +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -0,0 +1,5 @@ +features = "Full Support for Bot API 9.2" +[[pull_requests]] +uid = "4911" +author_uid = "aelkheir" +closes_threads = ["4910"] From d4ad9c0f46d72fa545c24d21035a2092300db9f3 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 17 Aug 2025 17:31:19 +0300 Subject: [PATCH 03/17] Use compact syntax for chango fragment. --- changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index 982353ce67a..771c28c63ba 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -1,5 +1,5 @@ features = "Full Support for Bot API 9.2" -[[pull_requests]] -uid = "4911" -author_uid = "aelkheir" -closes_threads = ["4910"] + +pull_requests = [ + { uid = "4911", author_uid = "aelkheir", closes_threads = ["4910"] }, +] From 423b14dbaad9b6a8bef194737cd9cdea8bb43ba1 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:53:02 -0400 Subject: [PATCH 04/17] Update chango fragment to include 4914 --- changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index 771c28c63ba..dbcb45415c8 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -2,4 +2,5 @@ features = "Full Support for Bot API 9.2" pull_requests = [ { uid = "4911", author_uid = "aelkheir", closes_threads = ["4910"] }, + { uid = "4914", author_uid = "harshil21"}, ] From 7886e7513e0a239743a1f8d8a25d1b65fe1d581d Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Mon, 18 Aug 2025 15:41:54 +0200 Subject: [PATCH 05/17] Api 9.2 checklist gifts (#4917) --- .../unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml | 8 +++++++- .../unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml | 5 +++++ src/telegram/_gifts.py | 13 +++++++++++++ src/telegram/_message.py | 11 +++++++++++ src/telegram/_reply.py | 17 +++++++++++++++-- src/telegram/_uniquegift.py | 13 +++++++++++++ tests/test_constants.py | 1 + tests/test_gifts.py | 13 ++++++++++++- tests/test_message.py | 2 ++ tests/test_reply.py | 5 +++++ tests/test_uniquegift.py | 8 ++++++++ 11 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 changes/unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index dbcb45415c8..42e19c99604 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -1,6 +1,12 @@ features = "Full Support for Bot API 9.2" pull_requests = [ - { uid = "4911", author_uid = "aelkheir", closes_threads = ["4910"] }, + { uid = "4911", author_uid = "aelkheir", closes_threads = ["4910"] }, + { uid = "4917", author_uid = "Poolitzer" }, + + + + { uid = "4914", author_uid = "harshil21"}, + ] diff --git a/changes/unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml b/changes/unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml new file mode 100644 index 00000000000..c9be36d763f --- /dev/null +++ b/changes/unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml @@ -0,0 +1,5 @@ +other = "Api 9.2 checklist gifts" +[[pull_requests]] +uid = "4917" +author_uid = "Poolitzer" +closes_threads = [] diff --git a/src/telegram/_gifts.py b/src/telegram/_gifts.py index 7c49aa1fd1e..5ed1a60f0f7 100644 --- a/src/telegram/_gifts.py +++ b/src/telegram/_gifts.py @@ -22,6 +22,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Optional +from telegram._chat import Chat from telegram._files.sticker import Sticker from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject @@ -53,6 +54,10 @@ class Gift(TelegramObject): to upgrade the gift to a unique one .. versionadded:: 21.10 + publisher_chat (:class:`telegram.Chat`, optional): Information about the chat that + published the gift. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`str`): Unique identifier of the gift @@ -66,11 +71,16 @@ class Gift(TelegramObject): to upgrade the gift to a unique one .. versionadded:: 21.10 + publisher_chat (:class:`telegram.Chat`): Optional. Information about the chat that + published the gift. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( "id", + "publisher_chat", "remaining_count", "star_count", "sticker", @@ -86,6 +96,7 @@ def __init__( total_count: Optional[int] = None, remaining_count: Optional[int] = None, upgrade_star_count: Optional[int] = None, + publisher_chat: Optional[Chat] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -96,6 +107,7 @@ def __init__( self.total_count: Optional[int] = total_count self.remaining_count: Optional[int] = remaining_count self.upgrade_star_count: Optional[int] = upgrade_star_count + self.publisher_chat: Optional[Chat] = publisher_chat self._id_attrs = (self.id,) @@ -107,6 +119,7 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Gift": data = cls._parse_data(data) data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) + data["publisher_chat"] = de_json_optional(data.get("publisher_chat"), Chat, bot) return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 16a4cd65ea3..f9e6d718318 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -628,6 +628,10 @@ class Message(MaybeInaccessibleMessage): of a channel has changed. .. versionadded:: 22.3 + reply_to_checklist_task_id (:obj:`int`, optional): Identifier of the specific checklist + task that is being replied to. + + .. versionadded:: NEXT.VERSION Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. In specific instances @@ -989,6 +993,10 @@ class Message(MaybeInaccessibleMessage): messages chat of a channel has changed. .. versionadded:: 22.3 + reply_to_checklist_task_id (:obj:`int`): Optional. Identifier of the specific checklist + task that is being replied to. + + .. versionadded:: NEXT.VERSION .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a @@ -1074,6 +1082,7 @@ class Message(MaybeInaccessibleMessage): "quote", "refunded_payment", "reply_markup", + "reply_to_checklist_task_id", "reply_to_message", "reply_to_story", "sender_boost_count", @@ -1195,6 +1204,7 @@ def __init__( checklist: Optional[Checklist] = None, checklist_tasks_done: Optional[ChecklistTasksDone] = None, checklist_tasks_added: Optional[ChecklistTasksAdded] = None, + reply_to_checklist_task_id: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1310,6 +1320,7 @@ def __init__( self.direct_message_price_changed: Optional[DirectMessagePriceChanged] = ( direct_message_price_changed ) + self.reply_to_checklist_task_id: Optional[int] = reply_to_checklist_task_id self._effective_attachment = DEFAULT_NONE diff --git a/src/telegram/_reply.py b/src/telegram/_reply.py index ae2165bd60e..bb9c72e7ea6 100644 --- a/src/telegram/_reply.py +++ b/src/telegram/_reply.py @@ -382,7 +382,8 @@ class ReplyParameters(TelegramObject): chat, or in the chat :paramref:`chat_id` if it is specified. chat_id (:obj:`int` | :obj:`str`, optional): If the message to be replied to is from a different chat, |chat_id_channel| - Not supported for messages sent on behalf of a business account. + Not supported for messages sent on behalf of a business account and messages from + channel direct messages chats. allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| Can be used only for replies in the same chat and forum topic. quote (:obj:`str`, optional): Quoted part of the message to be replied to; 0-1024 @@ -399,13 +400,18 @@ class ReplyParameters(TelegramObject): :paramref:`quote_parse_mode`. quote_position (:obj:`int`, optional): Position of the quote in the original message in UTF-16 code units. + checklist_task_id (:obj:`int`, optional): Identifier of the specific checklist task to be + replied to. + + .. versionadded:: NEXT.VERSION Attributes: message_id (:obj:`int`): Identifier of the message that will be replied to in the current chat, or in the chat :paramref:`chat_id` if it is specified. chat_id (:obj:`int` | :obj:`str`): Optional. If the message to be replied to is from a different chat, |chat_id_channel| - Not supported for messages sent on behalf of a business account. + Not supported for messages sent on behalf of a business account and messages from + channel direct messages chats. allow_sending_without_reply (:obj:`bool`): Optional. |allow_sending_without_reply| Can be used only for replies in the same chat and forum topic. quote (:obj:`str`): Optional. Quoted part of the message to be replied to; 0-1024 @@ -421,11 +427,16 @@ class ReplyParameters(TelegramObject): :paramref:`quote_parse_mode`. quote_position (:obj:`int`): Optional. Position of the quote in the original message in UTF-16 code units. + checklist_task_id (:obj:`int`): Optional. Identifier of the specific checklist task to be + replied to. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( "allow_sending_without_reply", "chat_id", + "checklist_task_id", "message_id", "quote", "quote_entities", @@ -438,6 +449,7 @@ def __init__( message_id: int, chat_id: Optional[Union[int, str]] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + checklist_task_id: Optional[int] = None, quote: Optional[str] = None, quote_parse_mode: ODVInput[str] = DEFAULT_NONE, quote_entities: Optional[Sequence[MessageEntity]] = None, @@ -456,6 +468,7 @@ def __init__( quote_entities ) self.quote_position: Optional[int] = quote_position + self.checklist_task_id: Optional[int] = checklist_task_id self._id_attrs = (self.message_id,) diff --git a/src/telegram/_uniquegift.py b/src/telegram/_uniquegift.py index 264b1ede4e1..ff825b987fd 100644 --- a/src/telegram/_uniquegift.py +++ b/src/telegram/_uniquegift.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Final, Optional from telegram import constants +from telegram._chat import Chat from telegram._files.sticker import Sticker from telegram._telegramobject import TelegramObject from telegram._utils import enum @@ -268,6 +269,10 @@ class UniqueGift(TelegramObject): model (:class:`UniqueGiftModel`): Model of the gift. symbol (:class:`UniqueGiftSymbol`): Symbol of the gift. backdrop (:class:`UniqueGiftBackdrop`): Backdrop of the gift. + publisher_chat (:class:`telegram.Chat`, optional): Information about the chat that + published the gift. + + .. versionadded:: NEXT.VERSION Attributes: base_name (:obj:`str`): Human-readable name of the regular gift from which this unique @@ -279,6 +284,10 @@ class UniqueGift(TelegramObject): model (:class:`telegram.UniqueGiftModel`): Model of the gift. symbol (:class:`telegram.UniqueGiftSymbol`): Symbol of the gift. backdrop (:class:`telegram.UniqueGiftBackdrop`): Backdrop of the gift. + publisher_chat (:class:`telegram.Chat`): Optional. Information about the chat that + published the gift. + + .. versionadded:: NEXT.VERSION """ @@ -288,6 +297,7 @@ class UniqueGift(TelegramObject): "model", "name", "number", + "publisher_chat", "symbol", ) @@ -299,6 +309,7 @@ def __init__( model: UniqueGiftModel, symbol: UniqueGiftSymbol, backdrop: UniqueGiftBackdrop, + publisher_chat: Optional[Chat] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -309,6 +320,7 @@ def __init__( self.model: UniqueGiftModel = model self.symbol: UniqueGiftSymbol = symbol self.backdrop: UniqueGiftBackdrop = backdrop + self.publisher_chat: Optional[Chat] = publisher_chat self._id_attrs = ( self.base_name, @@ -329,6 +341,7 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UniqueGift": data["model"] = de_json_optional(data.get("model"), UniqueGiftModel, bot) data["symbol"] = de_json_optional(data.get("symbol"), UniqueGiftSymbol, bot) data["backdrop"] = de_json_optional(data.get("backdrop"), UniqueGiftBackdrop, bot) + data["publisher_chat"] = de_json_optional(data.get("publisher_chat"), Chat, bot) return super().de_json(data=data, bot=bot) diff --git a/tests/test_constants.py b/tests/test_constants.py index b7cc6483627..9cec0de4643 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -204,6 +204,7 @@ def is_type_attribute(name: str) -> bool: "is_from_offline", "show_caption_above_media", "paid_star_count", + "reply_to_checklist_task_id", } @pytest.mark.parametrize( diff --git a/tests/test_gifts.py b/tests/test_gifts.py index e1f10d43564..b1f8190cfb2 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -20,7 +20,7 @@ import pytest -from telegram import BotCommand, Gift, GiftInfo, Gifts, MessageEntity, Sticker +from telegram import BotCommand, Chat, Gift, GiftInfo, Gifts, MessageEntity, Sticker from telegram._gifts import AcceptedGiftTypes from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.request import RequestData @@ -36,6 +36,7 @@ def gift(request): total_count=GiftTestBase.total_count, remaining_count=GiftTestBase.remaining_count, upgrade_star_count=GiftTestBase.upgrade_star_count, + publisher_chat=GiftTestBase.publisher_chat, ) @@ -54,6 +55,7 @@ class GiftTestBase: total_count = 10 remaining_count = 5 upgrade_star_count = 10 + publisher_chat = Chat(1, Chat.PRIVATE) class TestGiftWithoutRequest(GiftTestBase): @@ -70,6 +72,7 @@ def test_de_json(self, offline_bot, gift): "total_count": self.total_count, "remaining_count": self.remaining_count, "upgrade_star_count": self.upgrade_star_count, + "publisher_chat": self.publisher_chat.to_dict(), } gift = Gift.de_json(json_dict, offline_bot) assert gift.api_kwargs == {} @@ -80,6 +83,7 @@ def test_de_json(self, offline_bot, gift): assert gift.total_count == self.total_count assert gift.remaining_count == self.remaining_count assert gift.upgrade_star_count == self.upgrade_star_count + assert gift.publisher_chat == self.publisher_chat def test_to_dict(self, gift): gift_dict = gift.to_dict() @@ -91,6 +95,7 @@ def test_to_dict(self, gift): assert gift_dict["total_count"] == self.total_count assert gift_dict["remaining_count"] == self.remaining_count assert gift_dict["upgrade_star_count"] == self.upgrade_star_count + assert gift_dict["publisher_chat"] == self.publisher_chat.to_dict() def test_equality(self, gift): a = gift @@ -101,6 +106,7 @@ def test_equality(self, gift): self.total_count, self.remaining_count, self.upgrade_star_count, + self.publisher_chat, ) c = Gift( "other_uid", @@ -109,6 +115,7 @@ def test_equality(self, gift): self.total_count, self.remaining_count, self.upgrade_star_count, + self.publisher_chat, ) d = BotCommand("start", "description") @@ -210,6 +217,7 @@ class GiftsTestBase: total_count=5, remaining_count=5, upgrade_star_count=5, + publisher_chat=Chat(5, Chat.PRIVATE), ), Gift( id="id2", @@ -226,6 +234,7 @@ class GiftsTestBase: total_count=6, remaining_count=6, upgrade_star_count=6, + publisher_chat=Chat(6, Chat.PRIVATE), ), Gift( id="id3", @@ -242,6 +251,7 @@ class GiftsTestBase: total_count=7, remaining_count=7, upgrade_star_count=7, + publisher_chat=Chat(7, Chat.PRIVATE), ), ] @@ -265,6 +275,7 @@ def test_de_json(self, offline_bot, gifts): assert de_json_gift.total_count == original_gift.total_count assert de_json_gift.remaining_count == original_gift.remaining_count assert de_json_gift.upgrade_star_count == original_gift.upgrade_star_count + assert de_json_gift.publisher_chat == original_gift.publisher_chat def test_to_dict(self, gifts): gifts_dict = gifts.to_dict() diff --git a/tests/test_message.py b/tests/test_message.py index 1c8a12ac9c4..5efe1b393b9 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -355,6 +355,7 @@ def message(bot): tasks=[ChecklistTask(id=42, text="task 1"), ChecklistTask(id=43, text="task 2")], ) }, + {"reply_to_checklist_task_id": 11}, ], ids=[ "reply", @@ -436,6 +437,7 @@ def message(bot): "checklist", "checklist_tasks_done", "checklist_tasks_added", + "reply_to_checklist_task_id", ], ) def message_params(bot, request): diff --git a/tests/test_reply.py b/tests/test_reply.py index 0c144175640..4e1f3c3bf64 100644 --- a/tests/test_reply.py +++ b/tests/test_reply.py @@ -218,6 +218,7 @@ def reply_parameters(): quote_parse_mode=ReplyParametersTestBase.quote_parse_mode, quote_entities=ReplyParametersTestBase.quote_entities, quote_position=ReplyParametersTestBase.quote_position, + checklist_task_id=ReplyParametersTestBase.checklist_task_id, ) @@ -232,6 +233,7 @@ class ReplyParametersTestBase: MessageEntity(MessageEntity.EMAIL, 3, 4), ] quote_position = 5 + checklist_task_id = 9 class TestReplyParametersWithoutRequest(ReplyParametersTestBase): @@ -251,6 +253,7 @@ def test_de_json(self, offline_bot): "quote_parse_mode": self.quote_parse_mode, "quote_entities": [entity.to_dict() for entity in self.quote_entities], "quote_position": self.quote_position, + "checklist_task_id": self.checklist_task_id, } reply_parameters = ReplyParameters.de_json(json_dict, offline_bot) @@ -263,6 +266,7 @@ def test_de_json(self, offline_bot): assert reply_parameters.quote_parse_mode == self.quote_parse_mode assert reply_parameters.quote_entities == tuple(self.quote_entities) assert reply_parameters.quote_position == self.quote_position + assert reply_parameters.checklist_task_id == self.checklist_task_id def test_to_dict(self, reply_parameters): reply_parameters_dict = reply_parameters.to_dict() @@ -280,6 +284,7 @@ def test_to_dict(self, reply_parameters): entity.to_dict() for entity in self.quote_entities ] assert reply_parameters_dict["quote_position"] == self.quote_position + assert reply_parameters_dict["checklist_task_id"] == self.checklist_task_id def test_equality(self, reply_parameters): a = reply_parameters diff --git a/tests/test_uniquegift.py b/tests/test_uniquegift.py index bb317a0e9f7..3a21ad3fca9 100644 --- a/tests/test_uniquegift.py +++ b/tests/test_uniquegift.py @@ -23,6 +23,7 @@ from telegram import ( BotCommand, + Chat, Sticker, UniqueGift, UniqueGiftBackdrop, @@ -45,6 +46,7 @@ def unique_gift(): model=UniqueGiftTestBase.model, symbol=UniqueGiftTestBase.symbol, backdrop=UniqueGiftTestBase.backdrop, + publisher_chat=UniqueGiftTestBase.publisher_chat, ) @@ -67,6 +69,7 @@ class UniqueGiftTestBase: colors=UniqueGiftBackdropColors(0x00FF00, 0xEE00FF, 0xAA22BB, 0x20FE8F), rarity_per_mille=30, ) + publisher_chat = Chat(1, Chat.PRIVATE) class TestUniqueGiftWithoutRequest(UniqueGiftTestBase): @@ -83,6 +86,7 @@ def test_de_json(self, offline_bot): "model": self.model.to_dict(), "symbol": self.symbol.to_dict(), "backdrop": self.backdrop.to_dict(), + "publisher_chat": self.publisher_chat.to_dict(), } unique_gift = UniqueGift.de_json(json_dict, offline_bot) assert unique_gift.api_kwargs == {} @@ -93,6 +97,7 @@ def test_de_json(self, offline_bot): assert unique_gift.model == self.model assert unique_gift.symbol == self.symbol assert unique_gift.backdrop == self.backdrop + assert unique_gift.publisher_chat == self.publisher_chat def test_to_dict(self, unique_gift): gift_dict = unique_gift.to_dict() @@ -104,6 +109,7 @@ def test_to_dict(self, unique_gift): assert gift_dict["model"] == self.model.to_dict() assert gift_dict["symbol"] == self.symbol.to_dict() assert gift_dict["backdrop"] == self.backdrop.to_dict() + assert gift_dict["publisher_chat"] == self.publisher_chat.to_dict() def test_equality(self, unique_gift): a = unique_gift @@ -114,6 +120,7 @@ def test_equality(self, unique_gift): self.model, self.symbol, self.backdrop, + self.publisher_chat, ) c = UniqueGift( "other_base_name", @@ -122,6 +129,7 @@ def test_equality(self, unique_gift): self.model, self.symbol, self.backdrop, + self.publisher_chat, ) d = BotCommand("start", "description") From f0ac6800b323ff39a784b807be507ca1d4c81117 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 19 Aug 2025 02:19:32 -0400 Subject: [PATCH 06/17] API 9.2 Direct Messages in Channels (#4914) --- docs/source/telegram.at-tree.rst | 1 + docs/source/telegram.directmessagestopic.rst | 6 + docs/substitutions/global.rst | 2 + src/telegram/__init__.py | 2 + src/telegram/_bot.py | 97 ++++++++ src/telegram/_callbackquery.py | 1 + src/telegram/_chat.py | 69 +++++- src/telegram/_chatfullinfo.py | 22 ++ src/telegram/_directmessagestopic.py | 80 +++++++ src/telegram/_message.py | 63 ++++++ src/telegram/_user.py | 46 ++++ src/telegram/ext/_extbot.py | 42 ++++ src/telegram/ext/filters.py | 17 ++ tests/ext/test_filters.py | 7 + tests/test_bot.py | 10 + tests/test_callbackquery.py | 7 +- tests/test_chat.py | 5 + tests/test_chatfullinfo.py | 10 + tests/test_constants.py | 1 + tests/test_directmessagestopic.py | 88 ++++++++ tests/test_message.py | 222 +++++++++++++++---- 21 files changed, 750 insertions(+), 48 deletions(-) create mode 100644 docs/source/telegram.directmessagestopic.rst create mode 100644 src/telegram/_directmessagestopic.py create mode 100644 tests/test_directmessagestopic.py diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index acfaf866f46..109782eccd4 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -71,6 +71,7 @@ Available Types telegram.contact telegram.dice telegram.directmessagepricechanged + telegram.directmessagestopic telegram.document telegram.externalreplyinfo telegram.file diff --git a/docs/source/telegram.directmessagestopic.rst b/docs/source/telegram.directmessagestopic.rst new file mode 100644 index 00000000000..9779a021c91 --- /dev/null +++ b/docs/source/telegram.directmessagestopic.rst @@ -0,0 +1,6 @@ +DirectMessagesTopic +=================== + +.. autoclass:: telegram.DirectMessagesTopic + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index c161278591a..fd0bf74c7d2 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -96,6 +96,8 @@ .. |allow_paid_broadcast| replace:: Pass True to allow up to :tg-const:`telegram.constants.FloodLimit.PAID_MESSAGES_PER_SECOND` messages per second, ignoring `broadcasting limits `__ for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance. +.. |direct_messages_topic_id| replace:: Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat. + .. |tz-naive-dtms| replace:: For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. .. |org-verify| replace:: `on behalf of the organization `__ diff --git a/src/telegram/__init__.py b/src/telegram/__init__.py index b0277a7e77a..cc1f89b115b 100644 --- a/src/telegram/__init__.py +++ b/src/telegram/__init__.py @@ -93,6 +93,7 @@ "DataCredentials", "Dice", "DirectMessagePriceChanged", + "DirectMessagesTopic", "Document", "EncryptedCredentials", "EncryptedPassportElement", @@ -394,6 +395,7 @@ from ._copytextbutton import CopyTextButton from ._dice import Dice from ._directmessagepricechanged import DirectMessagePriceChanged +from ._directmessagestopic import DirectMessagesTopic from ._files._inputstorycontent import ( InputStoryContent, InputStoryContentPhoto, diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 33fba87e798..49016dee44b 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -758,6 +758,7 @@ async def _send_message( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -800,6 +801,7 @@ async def _send_message( "business_connection_id": business_connection_id, "caption": caption, "caption_entities": caption_entities, + "direct_messages_topic_id": direct_messages_topic_id, "disable_notification": disable_notification, "link_preview_options": link_preview_options, "message_thread_id": message_thread_id, @@ -1003,6 +1005,7 @@ async def send_message( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1057,6 +1060,8 @@ async def send_message( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1116,6 +1121,7 @@ async def send_message( reply_parameters=reply_parameters, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1232,6 +1238,7 @@ async def forward_message( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1267,6 +1274,12 @@ async def forward_message( message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 + direct_messages_topic_id (:obj:`int`, optional): Identifier of the direct messages + topic to which the message will be forwarded; required if the message is + forwarded to a direct messages chat. + + .. versionadded:: NEXT.VERSION + Returns: :class:`telegram.Message`: On success, the sent Message is returned. @@ -1292,6 +1305,7 @@ async def forward_message( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_messages( @@ -1302,6 +1316,7 @@ async def forward_messages( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1328,6 +1343,11 @@ async def forward_messages( disable_notification (:obj:`bool`, optional): |disable_notification| protect_content (:obj:`bool`, optional): |protect_content| message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + direct_messages_topic_id (:obj:`int`, optional): Identifier of the direct messages + topic to which the messages will be forwarded; required if the messages are + forwarded to a direct messages chat. + + .. versionadded:: NEXT.VERSION Returns: tuple[:class:`telegram.Message`]: On success, a tuple of ``MessageId`` of sent messages @@ -1343,6 +1363,7 @@ async def forward_messages( "disable_notification": disable_notification, "protect_content": protect_content, "message_thread_id": message_thread_id, + "direct_messages_topic_id": direct_messages_topic_id, } result = await self._post( @@ -1373,6 +1394,7 @@ async def send_photo( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1445,6 +1467,8 @@ async def send_photo( show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| .. versionadded:: 21.3 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1506,6 +1530,7 @@ async def send_photo( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_audio( @@ -1527,6 +1552,7 @@ async def send_audio( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1609,6 +1635,8 @@ async def send_audio( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1672,6 +1700,7 @@ async def send_audio( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_document( @@ -1691,6 +1720,7 @@ async def send_document( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1768,6 +1798,8 @@ async def send_document( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1827,6 +1859,7 @@ async def send_document( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_sticker( @@ -1842,6 +1875,7 @@ async def send_sticker( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1899,6 +1933,8 @@ async def send_sticker( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -1950,6 +1986,7 @@ async def send_sticker( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_video( @@ -1976,6 +2013,7 @@ async def send_video( show_caption_above_media: Optional[bool] = None, cover: Optional[FileInput] = None, start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2076,6 +2114,8 @@ async def send_video( show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| .. versionadded:: 21.3 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2144,6 +2184,7 @@ async def send_video( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_video_note( @@ -2161,6 +2202,7 @@ async def send_video_note( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2237,6 +2279,8 @@ async def send_video_note( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2296,6 +2340,7 @@ async def send_video_note( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_animation( @@ -2319,6 +2364,7 @@ async def send_animation( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2406,6 +2452,8 @@ async def send_animation( show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| .. versionadded:: 21.3 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2471,6 +2519,7 @@ async def send_animation( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_voice( @@ -2489,6 +2538,7 @@ async def send_voice( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2567,6 +2617,8 @@ async def send_voice( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2627,6 +2679,7 @@ async def send_voice( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_media_group( @@ -2642,6 +2695,7 @@ async def send_media_group( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2698,6 +2752,12 @@ async def send_media_group( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): Identifier of the direct messages + topic to which the messages will be sent; required if the messages are sent to a + direct messages chat. + + .. versionadded:: NEXT.VERSION + Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2793,6 +2853,7 @@ async def send_media_group( "business_connection_id": business_connection_id, "message_effect_id": message_effect_id, "allow_paid_broadcast": allow_paid_broadcast, + "direct_messages_topic_id": direct_messages_topic_id, } result = await self._post( @@ -2824,6 +2885,7 @@ async def send_location( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2890,6 +2952,8 @@ async def send_location( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -2961,6 +3025,7 @@ async def send_location( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def edit_message_live_location( @@ -3148,6 +3213,7 @@ async def send_venue( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3206,6 +3272,8 @@ async def send_venue( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -3288,6 +3356,7 @@ async def send_venue( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_contact( @@ -3305,6 +3374,7 @@ async def send_contact( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3353,6 +3423,8 @@ async def send_contact( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -3426,6 +3498,7 @@ async def send_contact( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_game( @@ -5231,6 +5304,7 @@ async def send_invoice( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -5351,6 +5425,8 @@ async def send_invoice( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -5421,6 +5497,7 @@ async def send_invoice( api_kwargs=api_kwargs, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def answer_shipping_query( @@ -7707,6 +7784,7 @@ async def send_dice( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -7759,6 +7837,8 @@ async def send_dice( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -7808,6 +7888,7 @@ async def send_dice( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def get_my_default_administrator_rights( @@ -8140,6 +8221,7 @@ async def copy_message( show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -8194,6 +8276,8 @@ async def copy_message( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -8253,6 +8337,7 @@ async def copy_message( "reply_parameters": reply_parameters, "show_caption_above_media": show_caption_above_media, "allow_paid_broadcast": allow_paid_broadcast, + "direct_messages_topic_id": direct_messages_topic_id, "video_start_timestamp": video_start_timestamp, } @@ -8276,6 +8361,7 @@ async def copy_messages( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, remove_caption: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -8308,6 +8394,12 @@ async def copy_messages( message_thread_id (:obj:`int`, optional): |message_thread_id_arg| remove_caption (:obj:`bool`, optional): Pass :obj:`True` to copy the messages without their captions. + direct_messages_topic_id (:obj:`int`, optional): Identifier of the direct messages + topic to which the message will be sent; required if the message is sent to a + direct messages chat. + + .. versionadded:: NEXT.VERSION + Returns: tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` @@ -8325,6 +8417,7 @@ async def copy_messages( "protect_content": protect_content, "message_thread_id": message_thread_id, "remove_caption": remove_caption, + "direct_messages_topic_id": direct_messages_topic_id, } result = await self._post( @@ -10729,6 +10822,7 @@ async def send_paid_media( business_connection_id: Optional[str] = None, payload: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -10775,6 +10869,8 @@ async def send_paid_media( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -10819,6 +10915,7 @@ async def send_paid_media( api_kwargs=api_kwargs, business_connection_id=business_connection_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def create_chat_subscription_invite_link( diff --git a/src/telegram/_callbackquery.py b/src/telegram/_callbackquery.py index 18b5980e6c6..6e98ff47462 100644 --- a/src/telegram/_callbackquery.py +++ b/src/telegram/_callbackquery.py @@ -885,6 +885,7 @@ async def copy_message( await update.callback_query.message.copy( from_chat_id=update.message.chat_id, message_id=update.message.message_id, + direct_messages_topic_id=update.message.direct_messages_topic.topic_id, *args, **kwargs ) diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index 53e4934523b..2dd89f5b76d 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -85,7 +85,16 @@ class _ChatBase(TelegramObject): .. versionadded:: 21.3 """ - __slots__ = ("first_name", "id", "is_forum", "last_name", "title", "type", "username") + __slots__ = ( + "first_name", + "id", + "is_direct_messages", + "is_forum", + "last_name", + "title", + "type", + "username", + ) def __init__( self, @@ -96,6 +105,7 @@ def __init__( first_name: Optional[str] = None, last_name: Optional[str] = None, is_forum: Optional[bool] = None, + is_direct_messages: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -109,6 +119,7 @@ def __init__( self.first_name: Optional[str] = first_name self.last_name: Optional[str] = last_name self.is_forum: Optional[bool] = is_forum + self.is_direct_messages: Optional[bool] = is_direct_messages self._id_attrs = (self.id,) @@ -1018,6 +1029,7 @@ async def send_message( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1060,6 +1072,7 @@ async def send_message( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def delete_message( @@ -1138,6 +1151,7 @@ async def send_media_group( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1181,6 +1195,7 @@ async def send_media_group( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_chat_action( @@ -1236,6 +1251,7 @@ async def send_photo( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1280,6 +1296,7 @@ async def send_photo( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_contact( @@ -1296,6 +1313,7 @@ async def send_contact( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1338,6 +1356,7 @@ async def send_contact( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_audio( @@ -1358,6 +1377,7 @@ async def send_audio( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1404,6 +1424,7 @@ async def send_audio( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_document( @@ -1422,6 +1443,7 @@ async def send_document( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1466,6 +1488,7 @@ async def send_document( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_checklist( @@ -1527,6 +1550,7 @@ async def send_dice( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1564,6 +1588,7 @@ async def send_dice( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_game( @@ -1646,6 +1671,7 @@ async def send_invoice( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1712,6 +1738,7 @@ async def send_invoice( reply_parameters=reply_parameters, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_location( @@ -1730,6 +1757,7 @@ async def send_location( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1774,6 +1802,7 @@ async def send_location( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_animation( @@ -1796,6 +1825,7 @@ async def send_animation( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1844,6 +1874,7 @@ async def send_animation( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_sticker( @@ -1858,6 +1889,7 @@ async def send_sticker( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1896,6 +1928,7 @@ async def send_sticker( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_venue( @@ -1916,6 +1949,7 @@ async def send_venue( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1962,6 +1996,7 @@ async def send_venue( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_video( @@ -1987,6 +2022,7 @@ async def send_video( show_caption_above_media: Optional[bool] = None, cover: Optional[FileInput] = None, start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2038,6 +2074,7 @@ async def send_video( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_video_note( @@ -2054,6 +2091,7 @@ async def send_video_note( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2096,6 +2134,7 @@ async def send_video_note( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_voice( @@ -2113,6 +2152,7 @@ async def send_voice( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2156,6 +2196,7 @@ async def send_voice( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_poll( @@ -2249,6 +2290,7 @@ async def send_copy( show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2292,6 +2334,7 @@ async def send_copy( message_thread_id=message_thread_id, show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def copy_message( @@ -2309,6 +2352,7 @@ async def copy_message( show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2352,6 +2396,7 @@ async def copy_message( message_thread_id=message_thread_id, show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_copies( @@ -2362,6 +2407,7 @@ async def send_copies( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, remove_caption: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2397,6 +2443,7 @@ async def send_copies( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=direct_messages_topic_id, ) async def copy_messages( @@ -2407,6 +2454,7 @@ async def copy_messages( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, remove_caption: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2442,6 +2490,7 @@ async def copy_messages( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_from( @@ -2452,6 +2501,7 @@ async def forward_from( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2486,6 +2536,7 @@ async def forward_from( api_kwargs=api_kwargs, protect_content=protect_content, message_thread_id=message_thread_id, + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_to( @@ -2496,6 +2547,7 @@ async def forward_to( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2531,6 +2583,7 @@ async def forward_to( api_kwargs=api_kwargs, protect_content=protect_content, message_thread_id=message_thread_id, + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_messages_from( @@ -2540,6 +2593,7 @@ async def forward_messages_from( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2574,6 +2628,7 @@ async def forward_messages_from( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_messages_to( @@ -2583,6 +2638,7 @@ async def forward_messages_to( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2617,6 +2673,7 @@ async def forward_messages_to( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=direct_messages_topic_id, ) async def export_invite_link( @@ -3456,6 +3513,7 @@ async def send_paid_media( business_connection_id: Optional[str] = None, payload: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3499,6 +3557,7 @@ async def send_paid_media( business_connection_id=business_connection_id, payload=payload, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_gift( @@ -3719,6 +3778,10 @@ class Chat(_ChatBase): (has topics_ enabled). .. versionadded:: 20.0 + is_direct_messages (:obj:`bool`, optional): :obj:`True`, if the chat is the direct messages + chat of a channel. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this chat. @@ -3733,6 +3796,10 @@ class Chat(_ChatBase): (has topics_ enabled). .. versionadded:: 20.0 + is_direct_messages (:obj:`bool`): Optional. :obj:`True`, if the chat is the direct messages + chat of a channel. + + .. versionadded:: NEXT.VERSION .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups """ diff --git a/src/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py index 67ef717832e..243787a6a13 100644 --- a/src/telegram/_chatfullinfo.py +++ b/src/telegram/_chatfullinfo.py @@ -224,6 +224,14 @@ class ChatFullInfo(_ChatBase): sent or forwarded to the channel chat. The field is available only for channel chats. .. versionadded:: 21.4 + is_direct_messages (:obj:`bool`, optional): :obj:`True`, if the chat is the direct messages + chat of a channel. + + .. versionadded:: NEXT.VERSION + parent_chat (:obj:`telegram.Chat`, optional): Information about the corresponding channel + chat; for direct messages chats only. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this chat. @@ -388,6 +396,14 @@ class ChatFullInfo(_ChatBase): sent or forwarded to the channel chat. The field is available only for channel chats. .. versionadded:: 21.4 + is_direct_messages (:obj:`bool`): Optional. :obj:`True`, if the chat is the direct messages + chat of a channel. + + .. versionadded:: NEXT.VERSION + parent_chat (:obj:`telegram.Chat`): Optional. Information about the corresponding channel + chat; for direct messages chats only. + + .. versionadded:: NEXT.VERSION .. _accent colors: https://core.telegram.org/bots/api#accent-colors .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups @@ -424,6 +440,7 @@ class ChatFullInfo(_ChatBase): "linked_chat_id", "location", "max_reaction_count", + "parent_chat", "permissions", "personal_chat", "photo", @@ -481,6 +498,8 @@ def __init__( linked_chat_id: Optional[int] = None, location: Optional[ChatLocation] = None, can_send_paid_media: Optional[bool] = None, + is_direct_messages: Optional[bool] = None, + parent_chat: Optional[Chat] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -492,6 +511,7 @@ def __init__( first_name=first_name, last_name=last_name, is_forum=is_forum, + is_direct_messages=is_direct_messages, api_kwargs=api_kwargs, ) # Required and unique to this class- @@ -546,6 +566,7 @@ def __init__( self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours self.can_send_paid_media: Optional[bool] = can_send_paid_media self.accepted_gift_types: AcceptedGiftTypes = accepted_gift_types + self.parent_chat: Optional[Chat] = parent_chat @property def slow_mode_delay(self) -> Optional[Union[int, dtm.timedelta]]: @@ -596,5 +617,6 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo": data["business_opening_hours"] = de_json_optional( data.get("business_opening_hours"), BusinessOpeningHours, bot ) + data["parent_chat"] = de_json_optional(data.get("parent_chat"), Chat, bot) return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_directmessagestopic.py b/src/telegram/_directmessagestopic.py new file mode 100644 index 00000000000..f8320476b42 --- /dev/null +++ b/src/telegram/_directmessagestopic.py @@ -0,0 +1,80 @@ +#!/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/]. +"""This module contains the DirectMessagesTopic class.""" + +from typing import TYPE_CHECKING, Optional + +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.argumentparsing import de_json_optional +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram._bot import Bot + + +class DirectMessagesTopic(TelegramObject): + """ + This class represents a topic for direct messages in a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`topic_id` and :attr:`user` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + topic_id (:obj:`int`): Unique identifier of the topic. + user (:class:`telegram.User`, optional): Information about the user that created the topic. + + .. hint:: + According to Telegram, this field is always present as of Bot API 9.2. + + Attributes: + topic_id (:obj:`int`): Unique identifier of the topic. + user (:class:`telegram.User`): Optional. Information about the user that created the topic. + + .. hint:: + According to Telegram, this field is always present as of Bot API 9.2. + + """ + + __slots__ = ("topic_id", "user") + + def __init__( + self, topic_id: int, user: Optional[User], *, api_kwargs: Optional[JSONDict] = None + ): + super().__init__(api_kwargs=api_kwargs) + + # Required: + self.topic_id: int = topic_id + + # Optionals: + self.user: Optional[User] = user + + self._id_attrs = (self.topic_id, self.user) + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "DirectMessagesTopic": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["user"] = de_json_optional(data.get("user"), User, bot) + + return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_message.py b/src/telegram/_message.py index f9e6d718318..9ebe9007cf6 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -31,6 +31,7 @@ from telegram._checklists import Checklist, ChecklistTasksAdded, ChecklistTasksDone from telegram._dice import Dice from telegram._directmessagepricechanged import DirectMessagePriceChanged +from telegram._directmessagestopic import DirectMessagesTopic from telegram._files.animation import Animation from telegram._files.audio import Audio from telegram._files.contact import Contact @@ -628,6 +629,10 @@ class Message(MaybeInaccessibleMessage): of a channel has changed. .. versionadded:: 22.3 + direct_messages_topic (:class:`telegram.DirectMessagesTopic`, optional): Information about + the direct messages chat topic that contains the message. + + .. versionadded:: NEXT.VERSION reply_to_checklist_task_id (:obj:`int`, optional): Identifier of the specific checklist task that is being replied to. @@ -993,6 +998,10 @@ class Message(MaybeInaccessibleMessage): messages chat of a channel has changed. .. versionadded:: 22.3 + direct_messages_topic (:class:`telegram.DirectMessagesTopic`): Optional. Information about + the direct messages chat topic that contains the message. + + .. versionadded:: NEXT.VERSION reply_to_checklist_task_id (:obj:`int`): Optional. Identifier of the specific checklist task that is being replied to. @@ -1034,6 +1043,7 @@ class Message(MaybeInaccessibleMessage): "delete_chat_photo", "dice", "direct_message_price_changed", + "direct_messages_topic", "document", "edit_date", "effect_id", @@ -1204,6 +1214,7 @@ def __init__( checklist: Optional[Checklist] = None, checklist_tasks_done: Optional[ChecklistTasksDone] = None, checklist_tasks_added: Optional[ChecklistTasksAdded] = None, + direct_messages_topic: Optional[DirectMessagesTopic] = None, reply_to_checklist_task_id: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, @@ -1320,6 +1331,7 @@ def __init__( self.direct_message_price_changed: Optional[DirectMessagePriceChanged] = ( direct_message_price_changed ) + self.direct_messages_topic: Optional[DirectMessagesTopic] = direct_messages_topic self.reply_to_checklist_task_id: Optional[int] = reply_to_checklist_task_id self._effective_attachment = DEFAULT_NONE @@ -1507,6 +1519,9 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Message": data["checklist_tasks_added"] = de_json_optional( data.get("checklist_tasks_added"), ChecklistTasksAdded, bot ) + data["direct_messages_topic"] = de_json_optional( + data.get("direct_messages_topic"), DirectMessagesTopic, bot + ) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility @@ -1863,6 +1878,10 @@ def _parse_message_thread_id( # the same chat. return self.message_thread_id if chat_id in {self.chat_id, self.chat.username} else None + def _extract_direct_messages_topic_id(self) -> Optional[int]: + """Return the topic id of the direct messages chat, if it is present.""" + return self.direct_messages_topic.topic_id if self.direct_messages_topic else None + async def reply_text( self, text: str, @@ -1893,6 +1912,7 @@ async def reply_text( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -1938,6 +1958,7 @@ async def reply_text( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_markdown( @@ -1970,6 +1991,7 @@ async def reply_markdown( message_thread_id=update.effective_message.message_thread_id, parse_mode=ParseMode.MARKDOWN, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2020,6 +2042,7 @@ async def reply_markdown( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_markdown_v2( @@ -2051,6 +2074,7 @@ async def reply_markdown_v2( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, parse_mode=ParseMode.MARKDOWN_V2, + direct_messages_topic_id=self.direct_messages_topic.topic_id, business_connection_id=self.business_connection_id, *args, **kwargs, @@ -2098,6 +2122,7 @@ async def reply_markdown_v2( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_html( @@ -2129,6 +2154,7 @@ async def reply_html( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, parse_mode=ParseMode.HTML, + direct_messages_topic_id=self.direct_messages_topic.topic_id, business_connection_id=self.business_connection_id, *args, **kwargs, @@ -2176,6 +2202,7 @@ async def reply_html( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_media_group( @@ -2208,6 +2235,7 @@ async def reply_media_group( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2253,6 +2281,7 @@ async def reply_media_group( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_photo( @@ -2287,6 +2316,7 @@ async def reply_photo( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2334,6 +2364,7 @@ async def reply_photo( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_audio( @@ -2370,6 +2401,7 @@ async def reply_audio( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2419,6 +2451,7 @@ async def reply_audio( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_document( @@ -2453,6 +2486,7 @@ async def reply_document( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2500,6 +2534,7 @@ async def reply_document( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_animation( @@ -2538,6 +2573,7 @@ async def reply_animation( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2589,6 +2625,7 @@ async def reply_animation( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_sticker( @@ -2618,6 +2655,7 @@ async def reply_sticker( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2660,6 +2698,7 @@ async def reply_sticker( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_video( @@ -2701,6 +2740,7 @@ async def reply_video( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2755,6 +2795,7 @@ async def reply_video( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_video_note( @@ -2787,6 +2828,7 @@ async def reply_video_note( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2832,6 +2874,7 @@ async def reply_video_note( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_voice( @@ -2865,6 +2908,7 @@ async def reply_voice( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2911,6 +2955,7 @@ async def reply_voice( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_location( @@ -2945,6 +2990,7 @@ async def reply_location( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -2992,6 +3038,7 @@ async def reply_location( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_venue( @@ -3028,6 +3075,7 @@ async def reply_venue( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -3077,6 +3125,7 @@ async def reply_venue( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_contact( @@ -3109,6 +3158,7 @@ async def reply_contact( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -3153,6 +3203,7 @@ async def reply_contact( message_thread_id=message_thread_id, business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), allow_paid_broadcast=allow_paid_broadcast, ) @@ -3277,6 +3328,7 @@ async def reply_dice( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, business_connection_id=self.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -3318,6 +3370,7 @@ async def reply_dice( business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_checklist( @@ -3537,6 +3590,7 @@ async def reply_invoice( await bot.send_invoice( update.effective_message.chat_id, message_thread_id=update.effective_message.message_thread_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs, ) @@ -3609,6 +3663,7 @@ async def reply_invoice( message_thread_id=message_thread_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def forward( @@ -3630,6 +3685,7 @@ async def forward( await bot.forward_message( from_chat_id=update.effective_message.chat_id, message_id=update.effective_message.message_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs ) @@ -3661,6 +3717,7 @@ async def forward( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def copy( @@ -3692,6 +3749,7 @@ async def copy( chat_id=chat_id, from_chat_id=update.effective_message.chat_id, message_id=update.effective_message.message_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs ) @@ -3724,6 +3782,7 @@ async def copy( message_thread_id=message_thread_id, show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_copy( @@ -3757,6 +3816,7 @@ async def reply_copy( chat_id=message.chat.id, message_thread_id=update.effective_message.message_thread_id, message_id=message_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs ) @@ -3802,6 +3862,7 @@ async def reply_copy( message_thread_id=message_thread_id, show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def reply_paid_media( @@ -3833,6 +3894,7 @@ async def reply_paid_media( await bot.send_paid_media( chat_id=message.chat.id, business_connection_id=message.business_connection_id, + direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, **kwargs ) @@ -3871,6 +3933,7 @@ async def reply_paid_media( protect_content=protect_content, show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=self._extract_direct_messages_topic_id(), ) async def edit_text( diff --git a/src/telegram/_user.py b/src/telegram/_user.py index ca9cd637193..4aa30afc1af 100644 --- a/src/telegram/_user.py +++ b/src/telegram/_user.py @@ -435,6 +435,7 @@ async def send_message( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, disable_web_page_preview: Optional[bool] = None, @@ -480,6 +481,7 @@ async def send_message( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def delete_message( @@ -562,6 +564,7 @@ async def send_photo( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -609,6 +612,7 @@ async def send_photo( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_media_group( @@ -623,6 +627,7 @@ async def send_media_group( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -669,6 +674,7 @@ async def send_media_group( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_audio( @@ -689,6 +695,7 @@ async def send_audio( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -738,6 +745,7 @@ async def send_audio( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_chat_action( @@ -794,6 +802,7 @@ async def send_contact( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -839,6 +848,7 @@ async def send_contact( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_dice( @@ -852,6 +862,7 @@ async def send_dice( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -892,6 +903,7 @@ async def send_dice( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_document( @@ -910,6 +922,7 @@ async def send_document( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -957,6 +970,7 @@ async def send_document( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_game( @@ -1042,6 +1056,7 @@ async def send_invoice( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1111,6 +1126,7 @@ async def send_invoice( message_thread_id=message_thread_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_location( @@ -1129,6 +1145,7 @@ async def send_location( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1176,6 +1193,7 @@ async def send_location( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_animation( @@ -1198,6 +1216,7 @@ async def send_animation( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1249,6 +1268,7 @@ async def send_animation( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_sticker( @@ -1263,6 +1283,7 @@ async def send_sticker( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1304,6 +1325,7 @@ async def send_sticker( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_video( @@ -1329,6 +1351,7 @@ async def send_video( show_caption_above_media: Optional[bool] = None, cover: Optional[FileInput] = None, start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1383,6 +1406,7 @@ async def send_video( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_venue( @@ -1403,6 +1427,7 @@ async def send_venue( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1452,6 +1477,7 @@ async def send_venue( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_video_note( @@ -1468,6 +1494,7 @@ async def send_video_note( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1513,6 +1540,7 @@ async def send_video_note( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_voice( @@ -1530,6 +1558,7 @@ async def send_voice( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1576,6 +1605,7 @@ async def send_voice( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_poll( @@ -1752,6 +1782,7 @@ async def send_copy( show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1796,6 +1827,7 @@ async def send_copy( message_thread_id=message_thread_id, show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def copy_message( @@ -1813,6 +1845,7 @@ async def copy_message( show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1857,6 +1890,7 @@ async def copy_message( message_thread_id=message_thread_id, show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_copies( @@ -1867,6 +1901,7 @@ async def send_copies( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, remove_caption: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1902,6 +1937,7 @@ async def send_copies( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=direct_messages_topic_id, ) async def copy_messages( @@ -1912,6 +1948,7 @@ async def copy_messages( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, remove_caption: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1947,6 +1984,7 @@ async def copy_messages( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_from( @@ -1957,6 +1995,7 @@ async def forward_from( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1991,6 +2030,7 @@ async def forward_from( api_kwargs=api_kwargs, protect_content=protect_content, message_thread_id=message_thread_id, + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_to( @@ -2001,6 +2041,7 @@ async def forward_to( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2036,6 +2077,7 @@ async def forward_to( api_kwargs=api_kwargs, protect_content=protect_content, message_thread_id=message_thread_id, + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_messages_from( @@ -2045,6 +2087,7 @@ async def forward_messages_from( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2079,6 +2122,7 @@ async def forward_messages_from( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_messages_to( @@ -2088,6 +2132,7 @@ async def forward_messages_to( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2122,6 +2167,7 @@ async def forward_messages_to( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + direct_messages_topic_id=direct_messages_topic_id, ) async def approve_join_request( diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index 1f9e14644c9..baace0f3673 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -617,6 +617,7 @@ async def _send_message( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -650,6 +651,7 @@ async def _send_message( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) if isinstance(result, Message): self._insert_callback_data(result) @@ -829,6 +831,7 @@ async def copy_message( show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -862,6 +865,7 @@ async def copy_message( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def copy_messages( @@ -873,6 +877,7 @@ async def copy_messages( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, remove_caption: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -895,6 +900,7 @@ async def copy_messages( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + direct_messages_topic_id=direct_messages_topic_id, ) async def get_chat( @@ -1768,6 +1774,7 @@ async def forward_message( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1789,6 +1796,7 @@ async def forward_message( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + direct_messages_topic_id=direct_messages_topic_id, ) async def forward_messages( @@ -1799,6 +1807,7 @@ async def forward_messages( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1814,6 +1823,7 @@ async def forward_messages( disable_notification=disable_notification, protect_content=protect_content, message_thread_id=message_thread_id, + direct_messages_topic_id=direct_messages_topic_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2466,6 +2476,7 @@ async def send_animation( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2505,6 +2516,7 @@ async def send_animation( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_audio( @@ -2526,6 +2538,7 @@ async def send_audio( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2563,6 +2576,7 @@ async def send_audio( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_chat_action( @@ -2606,6 +2620,7 @@ async def send_contact( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2638,6 +2653,7 @@ async def send_contact( business_connection_id=business_connection_id, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), message_effect_id=message_effect_id, + direct_messages_topic_id=direct_messages_topic_id, allow_paid_broadcast=allow_paid_broadcast, ) @@ -2719,6 +2735,7 @@ async def send_dice( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2747,6 +2764,7 @@ async def send_dice( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_document( @@ -2766,6 +2784,7 @@ async def send_document( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2801,6 +2820,7 @@ async def send_document( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_game( @@ -2876,6 +2896,7 @@ async def send_invoice( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2923,6 +2944,7 @@ async def send_invoice( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_location( @@ -2942,6 +2964,7 @@ async def send_location( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2977,6 +3000,7 @@ async def send_location( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_media_group( @@ -2992,6 +3016,7 @@ async def send_media_group( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3025,6 +3050,7 @@ async def send_media_group( caption_entities=caption_entities, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_message( @@ -3042,6 +3068,7 @@ async def send_message( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, disable_web_page_preview: Optional[bool] = None, reply_to_message_id: Optional[int] = None, @@ -3075,6 +3102,7 @@ async def send_message( link_preview_options=link_preview_options, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_photo( @@ -3094,6 +3122,7 @@ async def send_photo( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3129,6 +3158,7 @@ async def send_photo( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_poll( @@ -3212,6 +3242,7 @@ async def send_sticker( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3241,6 +3272,7 @@ async def send_sticker( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_venue( @@ -3262,6 +3294,7 @@ async def send_venue( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3299,6 +3332,7 @@ async def send_venue( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_video( @@ -3325,6 +3359,7 @@ async def send_video( show_caption_above_media: Optional[bool] = None, cover: Optional[FileInput] = None, start_timestamp: Optional[int] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3367,6 +3402,7 @@ async def send_video( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_video_note( @@ -3384,6 +3420,7 @@ async def send_video_note( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3417,6 +3454,7 @@ async def send_video_note( business_connection_id=business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def send_voice( @@ -3435,6 +3473,7 @@ async def send_voice( business_connection_id: Optional[str] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3468,6 +3507,7 @@ async def send_voice( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), business_connection_id=business_connection_id, message_effect_id=message_effect_id, + direct_messages_topic_id=direct_messages_topic_id, allow_paid_broadcast=allow_paid_broadcast, ) @@ -4907,6 +4947,7 @@ async def send_paid_media( business_connection_id: Optional[str] = None, payload: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + direct_messages_topic_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -4939,6 +4980,7 @@ async def send_paid_media( business_connection_id=business_connection_id, payload=payload, allow_paid_broadcast=allow_paid_broadcast, + direct_messages_topic_id=direct_messages_topic_id, ) async def create_chat_subscription_invite_link( diff --git a/src/telegram/ext/filters.py b/src/telegram/ext/filters.py index 94041114d73..d385c45987b 100644 --- a/src/telegram/ext/filters.py +++ b/src/telegram/ext/filters.py @@ -49,6 +49,7 @@ "CHECKLIST", "COMMAND", "CONTACT", + "DIRECT_MESSAGES", "EFFECT_ID", "FORWARDED", "GAME", @@ -1148,6 +1149,22 @@ def __init__(self, values: SCT[int]): """Dice messages with the emoji 🎰. Matches any dice value.""" +class _DirectMessages(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return bool(update.effective_chat and update.effective_chat.is_direct_messages) + + +DIRECT_MESSAGES = _DirectMessages(name="filters.DIRECT_MESSAGES") +"""Filter chats which are the direct messages for a channel. + +.. seealso:: :attr:`telegram.Chat.is_direct_messages` + +.. versionadded:: NEXT.VERSION +""" + + class Document: """ Subset for messages containing a document/file. diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index 48c2cf80d59..01716e37f30 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -2814,3 +2814,10 @@ def test_filters_checklist(self, update): update.message.checklist = "test" assert filters.CHECKLIST.check_update(update) assert str(filters.CHECKLIST) == "filters.CHECKLIST" + + def test_filters_direct_messages(self, update): + assert not filters.DIRECT_MESSAGES.check_update(update) + + update.message.chat.is_direct_messages = True + assert filters.DIRECT_MESSAGES.check_update(update) + assert str(filters.DIRECT_MESSAGES) == "filters.DIRECT_MESSAGES" diff --git a/tests/test_bot.py b/tests/test_bot.py index 6ecf041f77a..63e37013d47 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2373,6 +2373,16 @@ 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_direct_messages_topic_id_argument(self, offline_bot, monkeypatch): + """We 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("direct_messages_topic_id") == 42 + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.send_message(2, "text", direct_messages_topic_id=42) + 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_callbackquery.py b/tests/test_callbackquery.py index 6b759e885cb..0cf81c53cbb 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -576,11 +576,14 @@ async def make_assertion(*args, **kwargs): assert check_shortcut_signature( CallbackQuery.copy_message, Bot.copy_message, - ["message_id", "from_chat_id"], + ["message_id", "from_chat_id", "direct_messages_topic_id"], [], ) assert await check_shortcut_call( - callback_query.copy_message, callback_query.get_bot(), "copy_message" + callback_query.copy_message, + callback_query.get_bot(), + "copy_message", + shortcut_kwargs=["direct_messages_topic_id"], ) assert await check_defaults_handling(callback_query.copy_message, callback_query.get_bot()) diff --git a/tests/test_chat.py b/tests/test_chat.py index 4651393e473..d4b98c54dd5 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -49,6 +49,7 @@ def chat(bot): is_forum=True, first_name=ChatTestBase.first_name, last_name=ChatTestBase.last_name, + is_direct_messages=ChatTestBase.is_direct_messages, ) chat.set_bot(bot) chat._unfreeze() @@ -63,6 +64,7 @@ class ChatTestBase: is_forum = True first_name = "first" last_name = "last" + is_direct_messages = True class TestChatWithoutRequest(ChatTestBase): @@ -80,6 +82,7 @@ def test_de_json(self, offline_bot): "is_forum": self.is_forum, "first_name": self.first_name, "last_name": self.last_name, + "is_direct_messages": self.is_direct_messages, } chat = Chat.de_json(json_dict, offline_bot) @@ -90,6 +93,7 @@ def test_de_json(self, offline_bot): assert chat.is_forum == self.is_forum assert chat.first_name == self.first_name assert chat.last_name == self.last_name + assert chat.is_direct_messages == self.is_direct_messages def test_to_dict(self, chat): chat_dict = chat.to_dict() @@ -102,6 +106,7 @@ def test_to_dict(self, chat): assert chat_dict["is_forum"] == chat.is_forum assert chat_dict["first_name"] == chat.first_name assert chat_dict["last_name"] == chat.last_name + assert chat_dict["is_direct_messages"] == chat.is_direct_messages def test_enum_init(self): chat = Chat(id=1, type="foo") diff --git a/tests/test_chatfullinfo.py b/tests/test_chatfullinfo.py index ebcdd6a71cc..79d55e2fa8b 100644 --- a/tests/test_chatfullinfo.py +++ b/tests/test_chatfullinfo.py @@ -87,6 +87,8 @@ def chat_full_info(bot): first_name=ChatFullInfoTestBase.first_name, last_name=ChatFullInfoTestBase.last_name, can_send_paid_media=ChatFullInfoTestBase.can_send_paid_media, + is_direct_messages=ChatFullInfoTestBase.is_direct_messages, + parent_chat=ChatFullInfoTestBase.parent_chat, ) chat.set_bot(bot) chat._unfreeze() @@ -146,6 +148,8 @@ class ChatFullInfoTestBase: last_name = "last_name" can_send_paid_media = True accepted_gift_types = AcceptedGiftTypes(True, True, True, True) + is_direct_messages = True + parent_chat = Chat(4, "channel", "channel") class TestChatFullInfoWithoutRequest(ChatFullInfoTestBase): @@ -201,6 +205,8 @@ def test_de_json(self, offline_bot): "first_name": self.first_name, "last_name": self.last_name, "can_send_paid_media": self.can_send_paid_media, + "is_direct_messages": self.is_direct_messages, + "parent_chat": self.parent_chat.to_dict(), } cfi = ChatFullInfo.de_json(json_dict, offline_bot) @@ -250,6 +256,8 @@ def test_de_json(self, offline_bot): assert cfi.last_name == self.last_name assert cfi.max_reaction_count == self.max_reaction_count assert cfi.can_send_paid_media == self.can_send_paid_media + assert cfi.is_direct_messages == self.is_direct_messages + assert cfi.parent_chat == self.parent_chat def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): json_dict = { @@ -331,6 +339,8 @@ def test_to_dict(self, chat_full_info): assert cfi_dict["accepted_gift_types"] == cfi.accepted_gift_types.to_dict() assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count + assert cfi_dict["is_direct_messages"] == cfi.is_direct_messages + assert cfi_dict["parent_chat"] == cfi.parent_chat.to_dict() def test_time_period_properties(self, PTB_TIMEDELTA, chat_full_info): cfi = chat_full_info diff --git a/tests/test_constants.py b/tests/test_constants.py index 9cec0de4643..913d91d46b5 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -182,6 +182,7 @@ def is_type_attribute(name: str) -> bool: "caption", "chat", "chat_id", + "direct_messages_topic", "effective_attachment", "entities", "from_user", diff --git a/tests/test_directmessagestopic.py b/tests/test_directmessagestopic.py new file mode 100644 index 00000000000..6d086b4b104 --- /dev/null +++ b/tests/test_directmessagestopic.py @@ -0,0 +1,88 @@ +#!/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/]. +"""This module contains the TestDirectMessagesTopic class.""" + +import pytest + +from telegram import DirectMessagesTopic, User +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def direct_messages_topic(offline_bot): + dmt = DirectMessagesTopic( + topic_id=DirectMessagesTopicTestBase.topic_id, + user=DirectMessagesTopicTestBase.user, + ) + dmt.set_bot(offline_bot) + dmt._unfreeze() + return dmt + + +class DirectMessagesTopicTestBase: + topic_id = 12345 + user = User(id=67890, is_bot=False, first_name="Test") + + +class TestDirectMessagesTopicWithoutRequest(DirectMessagesTopicTestBase): + def test_slot_behaviour(self, direct_messages_topic): + cfi = direct_messages_topic + for attr in cfi.__slots__: + assert getattr(cfi, attr, "err") != "err", f"got extra slot '{attr}'" + + assert len(mro_slots(cfi)) == len(set(mro_slots(cfi))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "topic_id": self.topic_id, + "user": self.user.to_dict(), + } + + dmt = DirectMessagesTopic.de_json(json_dict, offline_bot) + assert dmt.topic_id == self.topic_id + assert dmt.user == self.user + assert dmt.api_kwargs == {} + + def test_to_dict(self, direct_messages_topic): + dmt = direct_messages_topic + dmt_dict = dmt.to_dict() + + assert isinstance(dmt_dict, dict) + assert dmt_dict["topic_id"] == dmt.topic_id + assert dmt_dict["user"] == dmt.user.to_dict() + + def test_equality(self, direct_messages_topic): + dmt_1 = direct_messages_topic + dmt_2 = DirectMessagesTopic( + topic_id=dmt_1.topic_id, + user=dmt_1.user, + ) + assert dmt_1 == dmt_2 + assert hash(dmt_1) == hash(dmt_2) + + random = User(id=99999, is_bot=False, first_name="Random") + assert random != dmt_2 + assert hash(random) != hash(dmt_2) + + dmt_3 = DirectMessagesTopic( + topic_id=8371, + user=dmt_1.user, + ) + assert dmt_1 != dmt_3 + assert hash(dmt_1) != hash(dmt_3) diff --git a/tests/test_message.py b/tests/test_message.py index 5efe1b393b9..4648cd8351d 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -91,6 +91,7 @@ Voice, WebAppData, ) +from telegram._directmessagestopic import DirectMessagesTopic from telegram._utils.datetime import UTC from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ODVInput @@ -355,6 +356,12 @@ def message(bot): tasks=[ChecklistTask(id=42, text="task 1"), ChecklistTask(id=43, text="task 2")], ) }, + { + "direct_messages_topic": DirectMessagesTopic( + topic_id=1234, + user=User(id=5678, first_name="TestUser", is_bot=False), + ) + }, {"reply_to_checklist_task_id": 11}, ], ids=[ @@ -437,6 +444,7 @@ def message(bot): "checklist", "checklist_tasks_done", "checklist_tasks_added", + "direct_messages_topic", "reply_to_checklist_task_id", ], ) @@ -1565,7 +1573,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_text, Bot.send_message, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1574,7 +1587,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1607,7 +1620,13 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_markdown, Bot.send_message, - ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "parse_mode", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1616,7 +1635,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1656,7 +1675,13 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_markdown_v2, Bot.send_message, - ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "parse_mode", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1665,7 +1690,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1708,7 +1733,13 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_html, Bot.send_message, - ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "parse_mode", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1717,7 +1748,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_message", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1745,7 +1776,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_media_group, Bot.send_media_group, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1754,7 +1790,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_media_group", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_media_group, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1787,7 +1823,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_photo, Bot.send_photo, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1796,7 +1837,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_photo", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_photo, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1821,7 +1862,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_audio, Bot.send_audio, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1830,7 +1876,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_audio", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_audio, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1855,7 +1901,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_document, Bot.send_document, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1864,7 +1915,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_document", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_document, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1889,7 +1940,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_animation, Bot.send_animation, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1898,7 +1954,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_animation", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_animation, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1923,7 +1979,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_sticker, Bot.send_sticker, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1932,7 +1993,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_sticker", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_sticker, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1957,7 +2018,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_video, Bot.send_video, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -1966,7 +2032,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_video", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_video, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -1991,7 +2057,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_video_note, Bot.send_video_note, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -2000,7 +2071,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_video_note", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_video_note, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -2025,7 +2096,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_voice, Bot.send_voice, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -2034,7 +2110,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_voice", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_voice, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -2059,7 +2135,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_location, Bot.send_location, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -2068,7 +2149,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_location", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_location, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -2093,7 +2174,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_venue, Bot.send_venue, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -2102,7 +2188,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_venue", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_venue, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -2127,7 +2213,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_contact, Bot.send_contact, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -2136,7 +2227,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_contact", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_contact, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -2171,7 +2262,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_poll", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_poll, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -2196,7 +2287,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_dice, Bot.send_dice, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -2205,7 +2301,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_dice", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_dice, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -2347,7 +2443,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_invoice, Bot.send_invoice, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) @@ -2356,7 +2457,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_invoice", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_invoice, message.get_bot(), no_default_kwargs={"message_thread_id"} @@ -2398,9 +2499,17 @@ async def make_assertion(*_, **kwargs): return chat_id and from_chat and message_id and notification and protected_cont assert check_shortcut_signature( - Message.forward, Bot.forward_message, ["from_chat_id", "message_id"], [] + Message.forward, + Bot.forward_message, + ["from_chat_id", "message_id", "direct_messages_topic_id"], + [], + ) + assert await check_shortcut_call( + message.forward, + message.get_bot(), + "forward_message", + shortcut_kwargs=["direct_messages_topic_id"], ) - assert await check_shortcut_call(message.forward, message.get_bot(), "forward_message") assert await check_defaults_handling(message.forward, message.get_bot()) monkeypatch.setattr(message.get_bot(), "forward_message", make_assertion) @@ -2433,9 +2542,17 @@ async def make_assertion(*_, **kwargs): ) assert check_shortcut_signature( - Message.copy, Bot.copy_message, ["from_chat_id", "message_id"], [] + Message.copy, + Bot.copy_message, + ["from_chat_id", "message_id", "direct_messages_topic_id"], + [], + ) + assert await check_shortcut_call( + message.copy, + message.get_bot(), + "copy_message", + shortcut_kwargs=["direct_messages_topic_id"], ) - assert await check_shortcut_call(message.copy, message.get_bot(), "copy_message") assert await check_defaults_handling(message.copy, message.get_bot()) monkeypatch.setattr(message.get_bot(), "copy_message", make_assertion) @@ -2476,11 +2593,21 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_copy, Bot.copy_message, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) - assert await check_shortcut_call(message.copy, message.get_bot(), "copy_message") + assert await check_shortcut_call( + message.copy, + message.get_bot(), + "copy_message", + shortcut_kwargs=["direct_messages_topic_id"], + ) assert await check_defaults_handling(message.copy, message.get_bot()) monkeypatch.setattr(message.get_bot(), "copy_message", make_assertion) @@ -2520,7 +2647,12 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.reply_paid_media, Bot.send_paid_media, - ["chat_id", "reply_to_message_id", "business_connection_id"], + [ + "chat_id", + "reply_to_message_id", + "business_connection_id", + "direct_messages_topic_id", + ], ["do_quote", "reply_to_message_id"], ) assert await check_shortcut_call( @@ -2528,7 +2660,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "send_paid_media", skip_params=["reply_to_message_id"], - shortcut_kwargs=["business_connection_id"], + shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"], ) assert await check_defaults_handling( message.reply_paid_media, message.get_bot(), no_default_kwargs={"message_thread_id"} From ffbc4c1e7c485e3dae4bea2e557d674570cd1d10 Mon Sep 17 00:00:00 2001 From: poolitzer Date: Tue, 19 Aug 2025 09:40:09 +0200 Subject: [PATCH 07/17] Fix: cleanup chango --- changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml | 7 +------ changes/unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml | 5 ----- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 changes/unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index 42e19c99604..be550dcb7c3 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -3,10 +3,5 @@ features = "Full Support for Bot API 9.2" pull_requests = [ { uid = "4911", author_uid = "aelkheir", closes_threads = ["4910"] }, { uid = "4917", author_uid = "Poolitzer" }, - - - - - { uid = "4914", author_uid = "harshil21"}, - + { uid = "4914", author_uid = "harshil21"}, ] diff --git a/changes/unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml b/changes/unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml deleted file mode 100644 index c9be36d763f..00000000000 --- a/changes/unreleased/4917.57KzS3RMc5kx3T3vg85rPi.toml +++ /dev/null @@ -1,5 +0,0 @@ -other = "Api 9.2 checklist gifts" -[[pull_requests]] -uid = "4917" -author_uid = "Poolitzer" -closes_threads = [] From a8b6ed74b45a53346aa930955fe1b6b63e69ef7e Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Wed, 27 Aug 2025 01:30:27 +0300 Subject: [PATCH 08/17] Add classes `SuggestedPost[Parameters/Price]`. (#4912) --- .../4911.kiF45Y4cfPGMq5cuLpa5da.toml | 1 + docs/source/telegram.at-tree.rst | 2 + .../telegram.suggestedpostparameters.rst | 6 + docs/source/telegram.suggestedpostprice.rst | 6 + docs/substitutions/global.rst | 2 + src/telegram/__init__.py | 3 + src/telegram/_bot.py | 123 +++++++++++++- src/telegram/_callbackquery.py | 3 + src/telegram/_chat.py | 39 +++++ src/telegram/_message.py | 43 +++++ src/telegram/_suggestedpost.py | 152 +++++++++++++++++ src/telegram/_user.py | 37 ++++ src/telegram/constants.py | 41 +++++ src/telegram/ext/_extbot.py | 37 ++++ tests/test_bot.py | 18 ++ tests/test_suggestedpost.py | 159 ++++++++++++++++++ 16 files changed, 671 insertions(+), 1 deletion(-) create mode 100644 docs/source/telegram.suggestedpostparameters.rst create mode 100644 docs/source/telegram.suggestedpostprice.rst create mode 100644 src/telegram/_suggestedpost.py create mode 100644 tests/test_suggestedpost.py diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index be550dcb7c3..6867011772d 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -4,4 +4,5 @@ pull_requests = [ { uid = "4911", author_uid = "aelkheir", closes_threads = ["4910"] }, { uid = "4917", author_uid = "Poolitzer" }, { uid = "4914", author_uid = "harshil21"}, + { uid = "4912", author_uid = "aelkheir" }, ] diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 109782eccd4..27afc79f654 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -169,6 +169,8 @@ Available Types telegram.storyareatypesuggestedreaction telegram.storyareatypeuniquegift telegram.storyareatypeweather + telegram.suggestedpostparameters + telegram.suggestedpostprice telegram.switchinlinequerychosenchat telegram.telegramobject telegram.textquote diff --git a/docs/source/telegram.suggestedpostparameters.rst b/docs/source/telegram.suggestedpostparameters.rst new file mode 100644 index 00000000000..5111d8fdd48 --- /dev/null +++ b/docs/source/telegram.suggestedpostparameters.rst @@ -0,0 +1,6 @@ +SuggestedPostParameters +======================= + +.. autoclass:: telegram.SuggestedPostParameters + :members: + :show-inheritance: diff --git a/docs/source/telegram.suggestedpostprice.rst b/docs/source/telegram.suggestedpostprice.rst new file mode 100644 index 00000000000..f5034e8f047 --- /dev/null +++ b/docs/source/telegram.suggestedpostprice.rst @@ -0,0 +1,6 @@ +SuggestedPostPrice +================== + +.. autoclass:: telegram.SuggestedPostPrice + :members: + :show-inheritance: diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index fd0bf74c7d2..fe7b976f5b7 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -98,6 +98,8 @@ .. |direct_messages_topic_id| replace:: Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat. +.. |suggested_post_parameters| replace:: An object containing the parameters of the suggested post to send; for direct messages chats only. If the message is sent as a reply to another suggested post, then that suggested post is automatically declined. + .. |tz-naive-dtms| replace:: For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. .. |org-verify| replace:: `on behalf of the organization `__ diff --git a/src/telegram/__init__.py b/src/telegram/__init__.py index cc1f89b115b..ccfc6c2d6ed 100644 --- a/src/telegram/__init__.py +++ b/src/telegram/__init__.py @@ -265,6 +265,8 @@ "StoryAreaTypeUniqueGift", "StoryAreaTypeWeather", "SuccessfulPayment", + "SuggestedPostParameters", + "SuggestedPostPrice", "SwitchInlineQueryChosenChat", "TelegramObject", "TextQuote", @@ -573,6 +575,7 @@ StoryAreaTypeUniqueGift, StoryAreaTypeWeather, ) +from ._suggestedpost import SuggestedPostParameters, SuggestedPostPrice from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject from ._uniquegift import ( diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 49016dee44b..81da34c35bd 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -136,6 +136,7 @@ PassportElementError, ShippingOption, StoryArea, + SuggestedPostParameters, ) BT = TypeVar("BT", bound="Bot") @@ -759,6 +760,7 @@ async def _send_message( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -810,6 +812,7 @@ async def _send_message( "protect_content": protect_content, "reply_markup": reply_markup, "reply_parameters": reply_parameters, + "suggested_post_parameters": suggested_post_parameters, } ) @@ -1006,6 +1009,7 @@ async def send_message( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1060,7 +1064,12 @@ async def send_message( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -1122,6 +1131,7 @@ async def send_message( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1239,6 +1249,7 @@ async def forward_message( message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1274,13 +1285,17 @@ async def forward_message( message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): An + object containing the parameters of the suggested post to send; for direct messages + chats only. + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): Identifier of the direct messages topic to which the message will be forwarded; required if the message is forwarded to a direct messages chat. .. versionadded:: NEXT.VERSION - Returns: :class:`telegram.Message`: On success, the sent Message is returned. @@ -1300,6 +1315,7 @@ async def forward_message( disable_notification=disable_notification, protect_content=protect_content, message_thread_id=message_thread_id, + suggested_post_parameters=suggested_post_parameters, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1395,6 +1411,7 @@ async def send_photo( allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1467,7 +1484,12 @@ async def send_photo( show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| .. versionadded:: 21.3 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -1531,6 +1553,7 @@ async def send_photo( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_audio( @@ -1553,6 +1576,7 @@ async def send_audio( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1635,7 +1659,12 @@ async def send_audio( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -1701,6 +1730,7 @@ async def send_audio( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_document( @@ -1721,6 +1751,7 @@ async def send_document( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1798,7 +1829,12 @@ async def send_document( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -1860,6 +1896,7 @@ async def send_document( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_sticker( @@ -1876,6 +1913,7 @@ async def send_sticker( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -1933,7 +1971,12 @@ async def send_sticker( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -1987,6 +2030,7 @@ async def send_sticker( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_video( @@ -2014,6 +2058,7 @@ async def send_video( cover: Optional[FileInput] = None, start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2114,7 +2159,12 @@ async def send_video( show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| .. versionadded:: 21.3 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -2185,6 +2235,7 @@ async def send_video( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_video_note( @@ -2203,6 +2254,7 @@ async def send_video_note( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2279,7 +2331,12 @@ async def send_video_note( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -2341,6 +2398,7 @@ async def send_video_note( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_animation( @@ -2365,6 +2423,7 @@ async def send_animation( allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2452,7 +2511,12 @@ async def send_animation( show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| .. versionadded:: 21.3 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -2520,6 +2584,7 @@ async def send_animation( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_voice( @@ -2539,6 +2604,7 @@ async def send_voice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2617,7 +2683,12 @@ async def send_voice( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -2680,6 +2751,7 @@ async def send_voice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_media_group( @@ -2886,6 +2958,7 @@ async def send_location( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2952,7 +3025,12 @@ async def send_location( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -3026,6 +3104,7 @@ async def send_location( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def edit_message_live_location( @@ -3214,6 +3293,7 @@ async def send_venue( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3272,7 +3352,12 @@ async def send_venue( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -3357,6 +3442,7 @@ async def send_venue( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_contact( @@ -3375,6 +3461,7 @@ async def send_contact( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3423,7 +3510,12 @@ async def send_contact( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -3499,6 +3591,7 @@ async def send_contact( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_game( @@ -5305,6 +5398,7 @@ async def send_invoice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -5425,7 +5519,12 @@ async def send_invoice( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -5498,6 +5597,7 @@ async def send_invoice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def answer_shipping_query( @@ -7785,6 +7885,7 @@ async def send_dice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -7837,7 +7938,12 @@ async def send_dice( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -7889,6 +7995,7 @@ async def send_dice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def get_my_default_administrator_rights( @@ -8222,6 +8329,7 @@ async def copy_message( allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -8276,7 +8384,12 @@ async def copy_message( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -8339,6 +8452,7 @@ async def copy_message( "allow_paid_broadcast": allow_paid_broadcast, "direct_messages_topic_id": direct_messages_topic_id, "video_start_timestamp": video_start_timestamp, + "suggested_post_parameters": suggested_post_parameters, } result = await self._post( @@ -10823,6 +10937,7 @@ async def send_paid_media( payload: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -10869,7 +10984,12 @@ async def send_paid_media( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional): + |suggested_post_parameters| + + .. versionadded:: NEXT.VERSION direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| + .. versionadded:: NEXT.VERSION Keyword Args: @@ -10916,6 +11036,7 @@ async def send_paid_media( business_connection_id=business_connection_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def create_chat_subscription_invite_link( diff --git a/src/telegram/_callbackquery.py b/src/telegram/_callbackquery.py index 6e98ff47462..7c6952889de 100644 --- a/src/telegram/_callbackquery.py +++ b/src/telegram/_callbackquery.py @@ -42,6 +42,7 @@ MessageEntity, MessageId, ReplyParameters, + SuggestedPostParameters, ) @@ -871,6 +872,7 @@ async def copy_message( show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -921,6 +923,7 @@ async def copy_message( reply_parameters=reply_parameters, show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, + suggested_post_parameters=suggested_post_parameters, ) MAX_ANSWER_TEXT_LENGTH: Final[int] = ( diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index 2dd89f5b76d..d360369a037 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -71,6 +71,7 @@ PhotoSize, ReplyParameters, Sticker, + SuggestedPostParameters, UserChatBoosts, Venue, Video, @@ -1030,6 +1031,7 @@ async def send_message( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1073,6 +1075,7 @@ async def send_message( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def delete_message( @@ -1252,6 +1255,7 @@ async def send_photo( allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1297,6 +1301,7 @@ async def send_photo( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_contact( @@ -1314,6 +1319,7 @@ async def send_contact( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1357,6 +1363,7 @@ async def send_contact( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_audio( @@ -1378,6 +1385,7 @@ async def send_audio( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1425,6 +1433,7 @@ async def send_audio( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_document( @@ -1444,6 +1453,7 @@ async def send_document( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1489,6 +1499,7 @@ async def send_document( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_checklist( @@ -1551,6 +1562,7 @@ async def send_dice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1589,6 +1601,7 @@ async def send_dice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_game( @@ -1672,6 +1685,7 @@ async def send_invoice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1739,6 +1753,7 @@ async def send_invoice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_location( @@ -1758,6 +1773,7 @@ async def send_location( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1803,6 +1819,7 @@ async def send_location( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_animation( @@ -1826,6 +1843,7 @@ async def send_animation( allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1875,6 +1893,7 @@ async def send_animation( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_sticker( @@ -1890,6 +1909,7 @@ async def send_sticker( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1929,6 +1949,7 @@ async def send_sticker( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_venue( @@ -1950,6 +1971,7 @@ async def send_venue( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1997,6 +2019,7 @@ async def send_venue( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_video( @@ -2023,6 +2046,7 @@ async def send_video( cover: Optional[FileInput] = None, start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2075,6 +2099,7 @@ async def send_video( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_video_note( @@ -2092,6 +2117,7 @@ async def send_video_note( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2135,6 +2161,7 @@ async def send_video_note( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_voice( @@ -2153,6 +2180,7 @@ async def send_voice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2197,6 +2225,7 @@ async def send_voice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_poll( @@ -2291,6 +2320,7 @@ async def send_copy( allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2335,6 +2365,7 @@ async def send_copy( show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def copy_message( @@ -2353,6 +2384,7 @@ async def copy_message( allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2397,6 +2429,7 @@ async def copy_message( show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_copies( @@ -2502,6 +2535,7 @@ async def forward_from( message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2537,6 +2571,7 @@ async def forward_from( protect_content=protect_content, message_thread_id=message_thread_id, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def forward_to( @@ -2548,6 +2583,7 @@ async def forward_to( message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2584,6 +2620,7 @@ async def forward_to( protect_content=protect_content, message_thread_id=message_thread_id, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def forward_messages_from( @@ -3514,6 +3551,7 @@ async def send_paid_media( payload: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3558,6 +3596,7 @@ async def send_paid_media( payload=payload, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_gift( diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 9ebe9007cf6..3902a4a97e4 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -119,6 +119,7 @@ MessageId, MessageOrigin, ReactionType, + SuggestedPostParameters, TextQuote, ) @@ -1895,6 +1896,7 @@ async def reply_text( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1959,6 +1961,7 @@ async def reply_text( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_markdown( @@ -1973,6 +1976,7 @@ async def reply_markdown( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2043,6 +2047,7 @@ async def reply_markdown( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_markdown_v2( @@ -2057,6 +2062,7 @@ async def reply_markdown_v2( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2123,6 +2129,7 @@ async def reply_markdown_v2( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_html( @@ -2137,6 +2144,7 @@ async def reply_html( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2203,6 +2211,7 @@ async def reply_html( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_media_group( @@ -2299,6 +2308,7 @@ async def reply_photo( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2365,6 +2375,7 @@ async def reply_photo( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_audio( @@ -2384,6 +2395,7 @@ async def reply_audio( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2452,6 +2464,7 @@ async def reply_audio( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_document( @@ -2469,6 +2482,7 @@ async def reply_document( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2535,6 +2549,7 @@ async def reply_document( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_animation( @@ -2556,6 +2571,7 @@ async def reply_animation( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2626,6 +2642,7 @@ async def reply_animation( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_sticker( @@ -2639,6 +2656,7 @@ async def reply_sticker( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2699,6 +2717,7 @@ async def reply_sticker( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_video( @@ -2723,6 +2742,7 @@ async def reply_video( show_caption_above_media: Optional[bool] = None, cover: Optional[FileInput] = None, start_timestamp: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2796,6 +2816,7 @@ async def reply_video( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_video_note( @@ -2811,6 +2832,7 @@ async def reply_video_note( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2875,6 +2897,7 @@ async def reply_video_note( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_voice( @@ -2891,6 +2914,7 @@ async def reply_voice( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2956,6 +2980,7 @@ async def reply_voice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_location( @@ -2973,6 +2998,7 @@ async def reply_location( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3039,6 +3065,7 @@ async def reply_location( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_venue( @@ -3058,6 +3085,7 @@ async def reply_venue( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3126,6 +3154,7 @@ async def reply_venue( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_contact( @@ -3141,6 +3170,7 @@ async def reply_contact( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3205,6 +3235,7 @@ async def reply_contact( message_effect_id=message_effect_id, direct_messages_topic_id=self._extract_direct_messages_topic_id(), allow_paid_broadcast=allow_paid_broadcast, + suggested_post_parameters=suggested_post_parameters, ) async def reply_poll( @@ -3312,6 +3343,7 @@ async def reply_dice( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3371,6 +3403,7 @@ async def reply_dice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_checklist( @@ -3575,6 +3608,7 @@ async def reply_invoice( reply_parameters: Optional["ReplyParameters"] = None, message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3664,6 +3698,7 @@ async def reply_invoice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def forward( @@ -3673,6 +3708,7 @@ async def forward( protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3712,6 +3748,7 @@ async def forward( disable_notification=disable_notification, protect_content=protect_content, message_thread_id=message_thread_id, + suggested_post_parameters=suggested_post_parameters, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -3734,6 +3771,7 @@ async def copy( show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3783,6 +3821,7 @@ async def copy( show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_copy( @@ -3800,6 +3839,7 @@ async def reply_copy( show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3863,6 +3903,7 @@ async def reply_copy( show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def reply_paid_media( @@ -3879,6 +3920,7 @@ async def reply_paid_media( reply_markup: Optional[ReplyMarkup] = None, payload: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3934,6 +3976,7 @@ async def reply_paid_media( show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + suggested_post_parameters=suggested_post_parameters, ) async def edit_text( diff --git a/src/telegram/_suggestedpost.py b/src/telegram/_suggestedpost.py new file mode 100644 index 00000000000..fc597e3aad1 --- /dev/null +++ b/src/telegram/_suggestedpost.py @@ -0,0 +1,152 @@ +#!/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/]. +"""This module contains objects related to Telegram suggested posts.""" + +import datetime as dtm +from typing import TYPE_CHECKING, Literal, Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import de_json_optional +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 SuggestedPostPrice(TelegramObject): + """ + Desribes the price of a suggested post. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`currency` and :attr:`amount` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + currency (:obj:`str`): + Currency in which the post will be paid. Currently, must be one of ``“XTR”`` for + Telegram Stars or ``“TON”`` for toncoins. + amount (:obj:`int`): + The amount of the currency that will be paid for the post in the smallest units of the + currency, i.e. Telegram Stars or nanotoncoins. Currently, price in Telegram Stars must + be between :tg-const:`telegram.constants.SuggestedPost.MIN_PRICE_STARS` + and :tg-const:`telegram.constants.SuggestedPost.MAX_PRICE_STARS`, and price in + nanotoncoins must be between + :tg-const:`telegram.constants.SuggestedPost.MIN_PRICE_NANOTONCOINS` + and :tg-const:`telegram.constants.SuggestedPost.MAX_PRICE_NANOTONCOINS`. + + Attributes: + currency (:obj:`str`): + Currency in which the post will be paid. Currently, must be one of ``“XTR”`` for + Telegram Stars or ``“TON”`` for toncoins. + amount (:obj:`int`): + The amount of the currency that will be paid for the post in the smallest units of the + currency, i.e. Telegram Stars or nanotoncoins. Currently, price in Telegram Stars must + be between :tg-const:`telegram.constants.SuggestedPost.MIN_PRICE_STARS` + and :tg-const:`telegram.constants.SuggestedPost.MAX_PRICE_STARS`, and price in + nanotoncoins must be between + :tg-const:`telegram.constants.SuggestedPost.MIN_PRICE_NANOTONCOINS` + and :tg-const:`telegram.constants.SuggestedPost.MAX_PRICE_NANOTONCOINS`. + """ + + __slots__ = ("amount", "currency") + + def __init__( + self, + currency: Literal["XTR", "TON"], + amount: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.currency: Literal["XTR", "TON"] = currency + self.amount: int = amount + + self._id_attrs = (self.currency, self.amount) + + self._freeze() + + +class SuggestedPostParameters(TelegramObject): + """ + Contains parameters of a post that is being suggested by the bot. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`price` and :attr:`send_date` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + price (:class:`telegram.SuggestedPostPrice`, optional): + Proposed price for the post. If the field is omitted, then the post is unpaid. + send_date (:class:`datetime.datetime`, optional): + Proposed send date of the post. If specified, then the date + must be between :tg-const:`telegram.constants.SuggestedPost.MIN_SEND_DATE` + second and :tg-const:`telegram.constants.SuggestedPost.MAX_SEND_DATE` seconds (30 days) + in the future. If the field is omitted, then the post can be published at any time + within :tg-const:`telegram.constants.SuggestedPost.MAX_SEND_DATE` seconds (30 days) at + the sole discretion of the user who approves it. + |datetime_localization| + + Attributes: + price (:class:`telegram.SuggestedPostPrice`): + Optional. Proposed price for the post. If the field is omitted, then the post + is unpaid. + send_date (:class:`datetime.datetime`): + Optional. Proposed send date of the post. If specified, then the date + must be between :tg-const:`telegram.constants.SuggestedPost.MIN_SEND_DATE` + second and :tg-const:`telegram.constants.SuggestedPost.MAX_SEND_DATE` seconds (30 days) + in the future. If the field is omitted, then the post can be published at any time + within :tg-const:`telegram.constants.SuggestedPost.MAX_SEND_DATE` seconds (30 days) at + the sole discretion of the user who approves it. + |datetime_localization| + + """ + + __slots__ = ("price", "send_date") + + def __init__( + self, + price: Optional[SuggestedPostPrice] = None, + send_date: Optional[dtm.datetime] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.price: Optional[SuggestedPostPrice] = price + self.send_date: Optional[dtm.datetime] = send_date + + self._id_attrs = (self.price, self.send_date) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostParameters": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["price"] = de_json_optional(data.get("price"), SuggestedPostPrice, bot) + + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + + data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) + + return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_user.py b/src/telegram/_user.py index 4aa30afc1af..3d49931ca1d 100644 --- a/src/telegram/_user.py +++ b/src/telegram/_user.py @@ -61,6 +61,7 @@ PhotoSize, ReplyParameters, Sticker, + SuggestedPostParameters, UserChatBoosts, UserProfilePhotos, Venue, @@ -436,6 +437,7 @@ async def send_message( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, disable_web_page_preview: Optional[bool] = None, @@ -482,6 +484,7 @@ async def send_message( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def delete_message( @@ -565,6 +568,7 @@ async def send_photo( allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -613,6 +617,7 @@ async def send_photo( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_media_group( @@ -696,6 +701,7 @@ async def send_audio( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -746,6 +752,7 @@ async def send_audio( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_chat_action( @@ -803,6 +810,7 @@ async def send_contact( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -849,6 +857,7 @@ async def send_contact( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_dice( @@ -863,6 +872,7 @@ async def send_dice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -904,6 +914,7 @@ async def send_dice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_document( @@ -923,6 +934,7 @@ async def send_document( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -971,6 +983,7 @@ async def send_document( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_game( @@ -1057,6 +1070,7 @@ async def send_invoice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1127,6 +1141,7 @@ async def send_invoice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_location( @@ -1146,6 +1161,7 @@ async def send_location( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1194,6 +1210,7 @@ async def send_location( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_animation( @@ -1217,6 +1234,7 @@ async def send_animation( allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1269,6 +1287,7 @@ async def send_animation( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_sticker( @@ -1284,6 +1303,7 @@ async def send_sticker( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1326,6 +1346,7 @@ async def send_sticker( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_video( @@ -1352,6 +1373,7 @@ async def send_video( cover: Optional[FileInput] = None, start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1407,6 +1429,7 @@ async def send_video( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_venue( @@ -1428,6 +1451,7 @@ async def send_venue( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1478,6 +1502,7 @@ async def send_venue( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_video_note( @@ -1495,6 +1520,7 @@ async def send_video_note( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1541,6 +1567,7 @@ async def send_video_note( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_voice( @@ -1559,6 +1586,7 @@ async def send_voice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1606,6 +1634,7 @@ async def send_voice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_poll( @@ -1783,6 +1812,7 @@ async def send_copy( allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1828,6 +1858,7 @@ async def send_copy( show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def copy_message( @@ -1846,6 +1877,7 @@ async def copy_message( allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1891,6 +1923,7 @@ async def copy_message( show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_copies( @@ -1996,6 +2029,7 @@ async def forward_from( message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2031,6 +2065,7 @@ async def forward_from( protect_content=protect_content, message_thread_id=message_thread_id, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def forward_to( @@ -2042,6 +2077,7 @@ async def forward_to( message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2078,6 +2114,7 @@ async def forward_to( protect_content=protect_content, message_thread_id=message_thread_id, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def forward_messages_from( diff --git a/src/telegram/constants.py b/src/telegram/constants.py index 435f1d8684c..0fdd801c017 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -117,6 +117,7 @@ "StoryAreaTypeLimit", "StoryAreaTypeType", "StoryLimit", + "SuggestedPost", "TransactionPartnerType", "TransactionPartnerUser", "UniqueGiftInfoOrigin", @@ -3054,6 +3055,46 @@ class StoryLimit(StringEnum): :meth:`telegram.Bot.post_story`.""" +class SuggestedPost(IntEnum): + """This enum contains limitations for :class:`telegram.SuggestedPostPrice`\ +/:class:`telegram.SuggestedPostParameters`. The enum + members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MIN_PRICE_STARS = 5 + """:obj:`int`: Minimum number of Telegram Stars in + :paramref:`~telegram.SuggestedPostPrice.amount` + parameter of :class:`telegram.SuggestedPostPrice`. + """ + MAX_PRICE_STARS = 100_000 + """:obj:`int`: Maximum number of Telegram Stars in + :paramref:`~telegram.SuggestedPostPrice.amount` + parameter of :class:`telegram.SuggestedPostPrice`. + """ + MIN_PRICE_NANOTONCOINS = 10_000_000 + """:obj:`int`: Minimum number of nanotoncoins in + :paramref:`~telegram.SuggestedPostPrice.amount` + parameter of :class:`telegram.SuggestedPostPrice`. + """ + MAX_PRICE_NANOTONCOINS = 10_000_000_000_000 + """:obj:`int`: Maximum number of nanotoncoins in + :paramref:`~telegram.SuggestedPostPrice.amount` + parameter of :class:`telegram.SuggestedPostPrice`. + """ + MIN_SEND_DATE = 300 + """:obj:`int`: Minimum number of seconds in the future for + the :paramref:`~telegram.SuggestedPostParameters.send_date` parameter of + :class:`telegram.SuggestedPostParameters`.""" + MAX_SEND_DATE = 2_678_400 + """:obj:`int`: Maximum number of seconds in the future for + the :paramref:`~telegram.SuggestedPostParameters.send_date` parameter of + :class:`telegram.SuggestedPostParameters`.""" + + class TransactionPartnerType(StringEnum): """This enum contains the available types of :class:`telegram.TransactionPartner`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index baace0f3673..0d543f6a830 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -129,6 +129,7 @@ PassportElementError, ShippingOption, StoryArea, + SuggestedPostParameters, ) from telegram.ext import BaseRateLimiter, Defaults @@ -618,6 +619,7 @@ async def _send_message( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -652,6 +654,7 @@ async def _send_message( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) if isinstance(result, Message): self._insert_callback_data(result) @@ -832,6 +835,7 @@ async def copy_message( allow_paid_broadcast: Optional[bool] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -866,6 +870,7 @@ async def copy_message( show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def copy_messages( @@ -1775,6 +1780,7 @@ async def forward_message( message_thread_id: Optional[int] = None, video_start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1791,6 +1797,7 @@ async def forward_message( disable_notification=disable_notification, protect_content=protect_content, message_thread_id=message_thread_id, + suggested_post_parameters=suggested_post_parameters, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2477,6 +2484,7 @@ async def send_animation( allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2517,6 +2525,7 @@ async def send_animation( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_audio( @@ -2539,6 +2548,7 @@ async def send_audio( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2577,6 +2587,7 @@ async def send_audio( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_chat_action( @@ -2621,6 +2632,7 @@ async def send_contact( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2655,6 +2667,7 @@ async def send_contact( message_effect_id=message_effect_id, direct_messages_topic_id=direct_messages_topic_id, allow_paid_broadcast=allow_paid_broadcast, + suggested_post_parameters=suggested_post_parameters, ) async def send_checklist( @@ -2736,6 +2749,7 @@ async def send_dice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2765,6 +2779,7 @@ async def send_dice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_document( @@ -2785,6 +2800,7 @@ async def send_document( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2821,6 +2837,7 @@ async def send_document( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_game( @@ -2897,6 +2914,7 @@ async def send_invoice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2945,6 +2963,7 @@ async def send_invoice( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_location( @@ -2965,6 +2984,7 @@ async def send_location( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3001,6 +3021,7 @@ async def send_location( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_media_group( @@ -3069,6 +3090,7 @@ async def send_message( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, disable_web_page_preview: Optional[bool] = None, reply_to_message_id: Optional[int] = None, @@ -3103,6 +3125,7 @@ async def send_message( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_photo( @@ -3123,6 +3146,7 @@ async def send_photo( allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3159,6 +3183,7 @@ async def send_photo( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_poll( @@ -3243,6 +3268,7 @@ async def send_sticker( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3273,6 +3299,7 @@ async def send_sticker( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_venue( @@ -3295,6 +3322,7 @@ async def send_venue( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3333,6 +3361,7 @@ async def send_venue( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_video( @@ -3360,6 +3389,7 @@ async def send_video( cover: Optional[FileInput] = None, start_timestamp: Optional[int] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3403,6 +3433,7 @@ async def send_video( allow_paid_broadcast=allow_paid_broadcast, show_caption_above_media=show_caption_above_media, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_video_note( @@ -3421,6 +3452,7 @@ async def send_video_note( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3455,6 +3487,7 @@ async def send_video_note( message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def send_voice( @@ -3474,6 +3507,7 @@ async def send_voice( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3509,6 +3543,7 @@ async def send_voice( message_effect_id=message_effect_id, direct_messages_topic_id=direct_messages_topic_id, allow_paid_broadcast=allow_paid_broadcast, + suggested_post_parameters=suggested_post_parameters, ) async def set_chat_administrator_custom_title( @@ -4948,6 +4983,7 @@ async def send_paid_media( payload: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, + suggested_post_parameters: Optional["SuggestedPostParameters"] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -4981,6 +5017,7 @@ async def send_paid_media( payload=payload, allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, + suggested_post_parameters=suggested_post_parameters, ) async def create_chat_subscription_invite_link( diff --git a/tests/test_bot.py b/tests/test_bot.py index 63e37013d47..00026b8f70e 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -75,6 +75,8 @@ ShippingOption, StarTransaction, StarTransactions, + SuggestedPostParameters, + SuggestedPostPrice, Update, User, WebAppInfo, @@ -2383,6 +2385,22 @@ 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", direct_messages_topic_id=42) + async def test_suggested_post_parameters_argument(self, offline_bot, monkeypatch): + """We can't test every single method easily, so we just test one. Our linting will catch + any unused args with the others.""" + suggested_post_parameters = SuggestedPostParameters(price=SuggestedPostPrice("TON", 10)) + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return ( + request_data.parameters.get("suggested_post_parameters") + == suggested_post_parameters.to_dict() + ) + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.send_message( + 2, "text", suggested_post_parameters=suggested_post_parameters + ) + 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_suggestedpost.py b/tests/test_suggestedpost.py new file mode 100644 index 00000000000..73fa9206779 --- /dev/null +++ b/tests/test_suggestedpost.py @@ -0,0 +1,159 @@ +#!/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 Dice +from telegram._suggestedpost import SuggestedPostParameters, SuggestedPostPrice +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def suggested_post_parameters(): + return SuggestedPostParameters( + price=SuggestedPostParametersTestBase.price, + send_date=SuggestedPostParametersTestBase.send_date, + ) + + +class SuggestedPostParametersTestBase: + price = SuggestedPostPrice(currency="XTR", amount=100) + send_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) + + +class TestSuggestedPostParametersWithoutRequest(SuggestedPostParametersTestBase): + def test_slot_behaviour(self, suggested_post_parameters): + for attr in suggested_post_parameters.__slots__: + assert getattr(suggested_post_parameters, attr, "err") != "err", ( + f"got extra slot '{attr}'" + ) + assert len(mro_slots(suggested_post_parameters)) == len( + set(mro_slots(suggested_post_parameters)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "price": self.price.to_dict(), + "send_date": to_timestamp(self.send_date), + } + spp = SuggestedPostParameters.de_json(json_dict, offline_bot) + assert spp.price == self.price + assert spp.send_date == self.send_date + assert spp.api_kwargs == {} + + def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): + json_dict = { + "price": self.price.to_dict(), + "send_date": to_timestamp(self.send_date), + } + + spp_bot = SuggestedPostParameters.de_json(json_dict, offline_bot) + spp_bot_raw = SuggestedPostParameters.de_json(json_dict, raw_bot) + spp_bot_tz = SuggestedPostParameters.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing tzinfo objects is not reliable + send_date_offset = spp_bot_tz.send_date.utcoffset() + send_date_offset_tz = tz_bot.defaults.tzinfo.utcoffset( + spp_bot_tz.send_date.replace(tzinfo=None) + ) + + assert spp_bot.send_date.tzinfo == UTC + assert spp_bot_raw.send_date.tzinfo == UTC + assert send_date_offset_tz == send_date_offset + + def test_to_dict(self, suggested_post_parameters): + spp_dict = suggested_post_parameters.to_dict() + + assert isinstance(spp_dict, dict) + assert spp_dict["price"] == self.price.to_dict() + assert spp_dict["send_date"] == to_timestamp(self.send_date) + + def test_equality(self, suggested_post_parameters): + a = suggested_post_parameters + b = SuggestedPostParameters(price=self.price, send_date=self.send_date) + c = SuggestedPostParameters( + price=self.price, send_date=self.send_date + dtm.timedelta(seconds=1) + ) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def suggested_post_price(): + return SuggestedPostPrice( + currency=SuggestedPostPriceTestBase.currency, + amount=SuggestedPostPriceTestBase.amount, + ) + + +class SuggestedPostPriceTestBase: + currency = "XTR" + amount = 100 + + +class TestSuggestedPostPriceWithoutRequest(SuggestedPostPriceTestBase): + def test_slot_behaviour(self, suggested_post_price): + for attr in suggested_post_price.__slots__: + assert getattr(suggested_post_price, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(suggested_post_price)) == len(set(mro_slots(suggested_post_price))), ( + "duplicate slot" + ) + + def test_de_json(self, offline_bot): + json_dict = { + "currency": self.currency, + "amount": self.amount, + } + spp = SuggestedPostPrice.de_json(json_dict, offline_bot) + assert spp.currency == self.currency + assert spp.amount == self.amount + assert spp.api_kwargs == {} + + def test_to_dict(self, suggested_post_price): + spp_dict = suggested_post_price.to_dict() + + assert isinstance(spp_dict, dict) + assert spp_dict["currency"] == self.currency + assert spp_dict["amount"] == self.amount + + def test_equality(self, suggested_post_price): + a = suggested_post_price + b = SuggestedPostPrice(currency=self.currency, amount=self.amount) + c = SuggestedPostPrice(currency="TON", amount=self.amount) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != e + assert hash(a) != hash(e) From 34517866842554b84ed35cf3e03b60238be4e133 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 5 Sep 2025 20:33:23 +0200 Subject: [PATCH 09/17] https://t.me/bot_api_changes/258 --- src/telegram/_directmessagestopic.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/telegram/_directmessagestopic.py b/src/telegram/_directmessagestopic.py index f8320476b42..c4eaed4f16a 100644 --- a/src/telegram/_directmessagestopic.py +++ b/src/telegram/_directmessagestopic.py @@ -39,14 +39,20 @@ class DirectMessagesTopic(TelegramObject): .. versionadded:: NEXT.VERSION Args: - topic_id (:obj:`int`): Unique identifier of the topic. + topic_id (:obj:`int`): Unique identifier of the topic. This number may have more than 32 + significant bits and some programming languages may have difficulty/silent defects in + interpreting it. But it has at most 52 significant bits, so a 64-bit integer or + double-precision float type are safe for storing this identifier. user (:class:`telegram.User`, optional): Information about the user that created the topic. .. hint:: According to Telegram, this field is always present as of Bot API 9.2. Attributes: - topic_id (:obj:`int`): Unique identifier of the topic. + topic_id (:obj:`int`): Unique identifier of the topic. This number may have more than 32 + significant bits and some programming languages may have difficulty/silent defects in + interpreting it. But it has at most 52 significant bits, so a 64-bit integer or + double-precision float type are safe for storing this identifier. user (:class:`telegram.User`): Optional. Information about the user that created the topic. .. hint:: From 90989a263aaf15431186a41f9ad411cb04b63b83 Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Fri, 5 Sep 2025 21:01:58 +0200 Subject: [PATCH 10/17] Feat: Added can_manage_dm and paid post (#4918) Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- .../4911.kiF45Y4cfPGMq5cuLpa5da.toml | 1 + src/telegram/_chatadministratorrights.py | 20 +++++++++++++++++-- src/telegram/_chatmember.py | 11 ++++++++++ src/telegram/_message.py | 11 ++++++++++ tests/test_chatadministratorrights.py | 5 +++++ tests/test_chatmember.py | 7 +++++++ tests/test_constants.py | 1 + tests/test_message.py | 2 ++ 8 files changed, 56 insertions(+), 2 deletions(-) diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index 6867011772d..b7f673cadc4 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -2,6 +2,7 @@ features = "Full Support for Bot API 9.2" pull_requests = [ { uid = "4911", author_uid = "aelkheir", closes_threads = ["4910"] }, + { uid = "4918", author_uid = "Poolitzer" }, { uid = "4917", author_uid = "Poolitzer" }, { uid = "4914", author_uid = "harshil21"}, { uid = "4912", author_uid = "aelkheir" }, diff --git a/src/telegram/_chatadministratorrights.py b/src/telegram/_chatadministratorrights.py index f5b75e786ac..311122d4da8 100644 --- a/src/telegram/_chatadministratorrights.py +++ b/src/telegram/_chatadministratorrights.py @@ -32,8 +32,8 @@ class ChatAdministratorRights(TelegramObject): :attr:`can_delete_messages`, :attr:`can_manage_video_chats`, :attr:`can_restrict_members`, :attr:`can_promote_members`, :attr:`can_change_info`, :attr:`can_invite_users`, :attr:`can_post_messages`, :attr:`can_edit_messages`, :attr:`can_pin_messages`, - :attr:`can_manage_topics`, :attr:`can_post_stories`, :attr:`can_delete_stories`, and - :attr:`can_edit_stories` are equal. + :attr:`can_manage_topics`, :attr:`can_post_stories`, :attr:`can_delete_stories`, + :attr:`can_edit_stories` and :attr:`can_manage_direct_messages` are equal. .. versionadded:: 20.0 @@ -50,6 +50,10 @@ class ChatAdministratorRights(TelegramObject): and :attr:`can_delete_stories` is now required. Thus, the order of arguments had to be changed. + .. versionchanged:: NEXT.VERSION + :attr:`can_manage_direct_messages` is considered as well when comparing objects of + this type in terms of equality. + 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 @@ -98,6 +102,10 @@ class ChatAdministratorRights(TelegramObject): to create, rename, close, and reopen forum topics; for supergroups only. .. versionadded:: 20.0 + can_manage_direct_messages (:obj:`bool`, optional): :obj:`True`, if the administrator can + manage direct messages of the channel and decline suggested posts; for channels only. + + .. versionadded:: NEXT.VERSION Attributes: is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. @@ -147,6 +155,10 @@ class ChatAdministratorRights(TelegramObject): to create, rename, close, and reopen forum topics; for supergroups only. .. versionadded:: 20.0 + can_manage_direct_messages (:obj:`bool`): Optional. :obj:`True`, if the administrator can + manage direct messages of the channel and decline suggested posts; for channels only. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -157,6 +169,7 @@ class ChatAdministratorRights(TelegramObject): "can_edit_stories", "can_invite_users", "can_manage_chat", + "can_manage_direct_messages", "can_manage_topics", "can_manage_video_chats", "can_pin_messages", @@ -184,6 +197,7 @@ def __init__( can_edit_messages: Optional[bool] = None, can_pin_messages: Optional[bool] = None, can_manage_topics: Optional[bool] = None, + can_manage_direct_messages: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: @@ -205,6 +219,7 @@ def __init__( self.can_edit_messages: Optional[bool] = can_edit_messages self.can_pin_messages: Optional[bool] = can_pin_messages self.can_manage_topics: Optional[bool] = can_manage_topics + self.can_manage_direct_messages: Optional[bool] = can_manage_direct_messages self._id_attrs = ( self.is_anonymous, @@ -222,6 +237,7 @@ def __init__( self.can_post_stories, self.can_edit_stories, self.can_delete_stories, + self.can_manage_direct_messages, ) self._freeze() diff --git a/src/telegram/_chatmember.py b/src/telegram/_chatmember.py index 647c089edde..2c77a944033 100644 --- a/src/telegram/_chatmember.py +++ b/src/telegram/_chatmember.py @@ -253,6 +253,10 @@ class ChatMemberAdministrator(ChatMember): .. versionadded:: 20.0 custom_title (:obj:`str`, optional): Custom title for this user. + can_manage_direct_messages (:obj:`bool`, optional): :obj:`True`, if the administrator can + manage direct messages of the channel and decline suggested posts; for channels only. + + .. versionadded:: NEXT.VERSION Attributes: status (:obj:`str`): The member's status in the chat, @@ -313,6 +317,10 @@ class ChatMemberAdministrator(ChatMember): .. versionadded:: 20.0 custom_title (:obj:`str`): Optional. Custom title for this user. + can_manage_direct_messages (:obj:`bool`, optional): :obj:`True`, if the administrator can + manage direct messages of the channel and decline suggested posts; for channels only. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -324,6 +332,7 @@ class ChatMemberAdministrator(ChatMember): "can_edit_stories", "can_invite_users", "can_manage_chat", + "can_manage_direct_messages", "can_manage_topics", "can_manage_video_chats", "can_pin_messages", @@ -355,6 +364,7 @@ def __init__( can_pin_messages: Optional[bool] = None, can_manage_topics: Optional[bool] = None, custom_title: Optional[str] = None, + can_manage_direct_messages: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -378,6 +388,7 @@ def __init__( self.can_pin_messages: Optional[bool] = can_pin_messages self.can_manage_topics: Optional[bool] = can_manage_topics self.custom_title: Optional[str] = custom_title + self.can_manage_direct_messages: Optional[bool] = can_manage_direct_messages class ChatMemberMember(ChatMember): diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 3902a4a97e4..21a4bba78c3 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -630,6 +630,10 @@ class Message(MaybeInaccessibleMessage): of a channel has changed. .. versionadded:: 22.3 + is_paid_post (:obj:`bool`, optional): :obj:`True`, if the message is a paid post. Note that + such posts must not be deleted for 24 hours to receive the payment and can't be edited. + + .. versionadded:: NEXT.VERSION direct_messages_topic (:class:`telegram.DirectMessagesTopic`, optional): Information about the direct messages chat topic that contains the message. @@ -999,6 +1003,10 @@ class Message(MaybeInaccessibleMessage): messages chat of a channel has changed. .. versionadded:: 22.3 + is_paid_post (:obj:`bool`): Optional. :obj:`True`, if the message is a paid post. Note that + such posts must not be deleted for 24 hours to receive the payment and can't be edited. + + .. versionadded:: NEXT.VERSION direct_messages_topic (:class:`telegram.DirectMessagesTopic`): Optional. Information about the direct messages chat topic that contains the message. @@ -1070,6 +1078,7 @@ class Message(MaybeInaccessibleMessage): "invoice", "is_automatic_forward", "is_from_offline", + "is_paid_post", "is_topic_message", "left_chat_member", "link_preview_options", @@ -1215,6 +1224,7 @@ def __init__( checklist: Optional[Checklist] = None, checklist_tasks_done: Optional[ChecklistTasksDone] = None, checklist_tasks_added: Optional[ChecklistTasksAdded] = None, + is_paid_post: Optional[bool] = None, direct_messages_topic: Optional[DirectMessagesTopic] = None, reply_to_checklist_task_id: Optional[int] = None, *, @@ -1332,6 +1342,7 @@ def __init__( self.direct_message_price_changed: Optional[DirectMessagePriceChanged] = ( direct_message_price_changed ) + self.is_paid_post: Optional[bool] = is_paid_post self.direct_messages_topic: Optional[DirectMessagesTopic] = direct_messages_topic self.reply_to_checklist_task_id: Optional[int] = reply_to_checklist_task_id diff --git a/tests/test_chatadministratorrights.py b/tests/test_chatadministratorrights.py index c93f23d4bcd..76a6f06e7cc 100644 --- a/tests/test_chatadministratorrights.py +++ b/tests/test_chatadministratorrights.py @@ -40,6 +40,7 @@ def chat_admin_rights(): can_post_stories=True, can_edit_stories=True, can_delete_stories=True, + can_manage_direct_messages=True, ) @@ -67,6 +68,7 @@ def test_de_json(self, offline_bot, chat_admin_rights): "can_post_stories": True, "can_edit_stories": True, "can_delete_stories": True, + "can_manage_direct_messages": True, } chat_administrator_rights_de = ChatAdministratorRights.de_json(json_dict, offline_bot) assert chat_administrator_rights_de.api_kwargs == {} @@ -93,6 +95,7 @@ def test_to_dict(self, chat_admin_rights): assert admin_rights_dict["can_post_stories"] == car.can_post_stories assert admin_rights_dict["can_edit_stories"] == car.can_edit_stories assert admin_rights_dict["can_delete_stories"] == car.can_delete_stories + assert admin_rights_dict["can_manage_direct_messages"] == car.can_manage_direct_messages def test_equality(self): a = ChatAdministratorRights( @@ -143,6 +146,7 @@ def test_all_rights(self): True, True, True, + True, ) t = ChatAdministratorRights.all_rights() # if the dirs are the same, the attributes will all be there @@ -168,6 +172,7 @@ def test_no_rights(self): False, False, False, + False, ) t = ChatAdministratorRights.no_rights() # if the dirs are the same, the attributes will all be there diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index fdf6136f701..5453e470b2b 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -75,6 +75,7 @@ class ChatMemberTestBase: can_send_voice_notes = True can_send_messages = True is_member = True + can_manage_direct_messages = True class TestChatMemberWithoutRequest(ChatMemberTestBase): @@ -170,6 +171,7 @@ def chat_member_administrator(): TestChatMemberAdministratorWithoutRequest.can_restrict_members, TestChatMemberAdministratorWithoutRequest.custom_title, TestChatMemberAdministratorWithoutRequest.is_anonymous, + TestChatMemberAdministratorWithoutRequest.can_manage_direct_messages, ) @@ -202,6 +204,7 @@ def test_de_json(self, offline_bot): "can_restrict_members": self.can_restrict_members, "custom_title": self.custom_title, "is_anonymous": self.is_anonymous, + "can_manage_direct_messages": self.can_manage_direct_messages, } chat_member = ChatMemberAdministrator.de_json(data, offline_bot) @@ -226,6 +229,7 @@ def test_de_json(self, offline_bot): assert chat_member.can_restrict_members == self.can_restrict_members assert chat_member.custom_title == self.custom_title assert chat_member.is_anonymous == self.is_anonymous + assert chat_member.can_manage_direct_messages == self.can_manage_direct_messages def test_to_dict(self, chat_member_administrator): assert chat_member_administrator.to_dict() == { @@ -248,6 +252,7 @@ def test_to_dict(self, chat_member_administrator): "can_restrict_members": chat_member_administrator.can_restrict_members, "custom_title": chat_member_administrator.custom_title, "is_anonymous": chat_member_administrator.is_anonymous, + "can_manage_direct_messages": chat_member_administrator.can_manage_direct_messages, } def test_equality(self, chat_member_administrator): @@ -266,6 +271,7 @@ def test_equality(self, chat_member_administrator): True, True, True, + True, ) c = ChatMemberAdministrator( User(1, "test_user", is_bot=False), @@ -281,6 +287,7 @@ def test_equality(self, chat_member_administrator): False, False, False, + False, ) d = Dice(5, "test") diff --git a/tests/test_constants.py b/tests/test_constants.py index 913d91d46b5..db988a3d889 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -205,6 +205,7 @@ def is_type_attribute(name: str) -> bool: "is_from_offline", "show_caption_above_media", "paid_star_count", + "is_paid_post", "reply_to_checklist_task_id", } diff --git a/tests/test_message.py b/tests/test_message.py index 4648cd8351d..96dc063c120 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -356,6 +356,7 @@ def message(bot): tasks=[ChecklistTask(id=42, text="task 1"), ChecklistTask(id=43, text="task 2")], ) }, + {"is_paid_post": True}, { "direct_messages_topic": DirectMessagesTopic( topic_id=1234, @@ -444,6 +445,7 @@ def message(bot): "checklist", "checklist_tasks_done", "checklist_tasks_added", + "is_paid_post", "direct_messages_topic", "reply_to_checklist_task_id", ], From 32c1a852bad96b98db2d2511fbf18008809645a1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Fri, 5 Sep 2025 22:06:01 +0300 Subject: [PATCH 11/17] Add classes `SuggestedPost[Declined,Paid,Refunded]` (#4921) --- .../4911.kiF45Y4cfPGMq5cuLpa5da.toml | 1 + docs/source/telegram.at-tree.rst | 3 + .../source/telegram.suggestedpostdeclined.rst | 6 + docs/source/telegram.suggestedpostpaid.rst | 6 + .../source/telegram.suggestedpostrefunded.rst | 6 + src/telegram/__init__.py | 11 +- src/telegram/_message.py | 50 +++++ src/telegram/_suggestedpost.py | 207 ++++++++++++++++++ src/telegram/constants.py | 32 +++ src/telegram/ext/filters.py | 40 ++++ tests/ext/test_filters.py | 15 ++ tests/test_message.py | 29 +++ tests/test_suggestedpost.py | 203 ++++++++++++++++- 13 files changed, 607 insertions(+), 2 deletions(-) create mode 100644 docs/source/telegram.suggestedpostdeclined.rst create mode 100644 docs/source/telegram.suggestedpostpaid.rst create mode 100644 docs/source/telegram.suggestedpostrefunded.rst diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index b7f673cadc4..36f013cff7c 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -6,4 +6,5 @@ pull_requests = [ { uid = "4917", author_uid = "Poolitzer" }, { uid = "4914", author_uid = "harshil21"}, { uid = "4912", author_uid = "aelkheir" }, + { uid = "4921", author_uid = "aelkheir" }, ] diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 27afc79f654..9343034d9a8 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -169,8 +169,11 @@ Available Types telegram.storyareatypesuggestedreaction telegram.storyareatypeuniquegift telegram.storyareatypeweather + telegram.suggestedpostdeclined + telegram.suggestedpostpaid telegram.suggestedpostparameters telegram.suggestedpostprice + telegram.suggestedpostrefunded telegram.switchinlinequerychosenchat telegram.telegramobject telegram.textquote diff --git a/docs/source/telegram.suggestedpostdeclined.rst b/docs/source/telegram.suggestedpostdeclined.rst new file mode 100644 index 00000000000..bf9194d074b --- /dev/null +++ b/docs/source/telegram.suggestedpostdeclined.rst @@ -0,0 +1,6 @@ +SuggestedPostDeclined +===================== + +.. autoclass:: telegram.SuggestedPostDeclined + :members: + :show-inheritance: diff --git a/docs/source/telegram.suggestedpostpaid.rst b/docs/source/telegram.suggestedpostpaid.rst new file mode 100644 index 00000000000..6eb4a57bbda --- /dev/null +++ b/docs/source/telegram.suggestedpostpaid.rst @@ -0,0 +1,6 @@ +SuggestedPostPaid +================= + +.. autoclass:: telegram.SuggestedPostPaid + :members: + :show-inheritance: diff --git a/docs/source/telegram.suggestedpostrefunded.rst b/docs/source/telegram.suggestedpostrefunded.rst new file mode 100644 index 00000000000..2fb5ad1beff --- /dev/null +++ b/docs/source/telegram.suggestedpostrefunded.rst @@ -0,0 +1,6 @@ +SuggestedPostRefunded +===================== + +.. autoclass:: telegram.SuggestedPostRefunded + :members: + :show-inheritance: diff --git a/src/telegram/__init__.py b/src/telegram/__init__.py index ccfc6c2d6ed..4bf21bcc4da 100644 --- a/src/telegram/__init__.py +++ b/src/telegram/__init__.py @@ -265,8 +265,11 @@ "StoryAreaTypeUniqueGift", "StoryAreaTypeWeather", "SuccessfulPayment", + "SuggestedPostDeclined", + "SuggestedPostPaid", "SuggestedPostParameters", "SuggestedPostPrice", + "SuggestedPostRefunded", "SwitchInlineQueryChosenChat", "TelegramObject", "TextQuote", @@ -575,7 +578,13 @@ StoryAreaTypeUniqueGift, StoryAreaTypeWeather, ) -from ._suggestedpost import SuggestedPostParameters, SuggestedPostPrice +from ._suggestedpost import ( + SuggestedPostDeclined, + SuggestedPostPaid, + SuggestedPostParameters, + SuggestedPostPrice, + SuggestedPostRefunded, +) from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject from ._uniquegift import ( diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 21a4bba78c3..d7ca62fbf60 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -119,7 +119,10 @@ MessageId, MessageOrigin, ReactionType, + SuggestedPostDeclined, + SuggestedPostPaid, SuggestedPostParameters, + SuggestedPostRefunded, TextQuote, ) @@ -568,6 +571,18 @@ class Message(MaybeInaccessibleMessage): message: the price for paid messages has changed in the chat .. versionadded:: 22.1 + suggested_post_declined (:class:`telegram.SuggestedPostDeclined`, optional): Service + message: a suggested post was declined. + + .. versionadded:: NEXT.VERSION + suggested_post_paid (:class:`telegram.SuggestedPostPaid`, optional): Service + message: payment for a suggested post was received. + + .. versionadded:: NEXT.VERSION + suggested_post_refunded (:class:`telegram.SuggestedPostRefunded`, optional): Service + message: payment for a suggested post was refunded. + + .. versionadded:: NEXT.VERSION external_reply (:class:`telegram.ExternalReplyInfo`, optional): Information about the message that is being replied to, which may come from another chat or forum topic. @@ -940,6 +955,18 @@ class Message(MaybeInaccessibleMessage): message: the price for paid messages has changed in the chat .. versionadded:: 22.1 + suggested_post_declined (:class:`telegram.SuggestedPostDeclined`): Optional. Service + message: a suggested post was declined. + + .. versionadded:: NEXT.VERSION + suggested_post_paid (:class:`telegram.SuggestedPostPaid`): Optional. Service + message: payment for a suggested post was received. + + .. versionadded:: NEXT.VERSION + suggested_post_refunded (:class:`telegram.SuggestedPostRefunded`): Optional. Service + message: payment for a suggested post was refunded. + + .. versionadded:: NEXT.VERSION external_reply (:class:`telegram.ExternalReplyInfo`): Optional. Information about the message that is being replied to, which may come from another chat or forum topic. @@ -1112,6 +1139,9 @@ class Message(MaybeInaccessibleMessage): "sticker", "story", "successful_payment", + "suggested_post_declined", + "suggested_post_paid", + "suggested_post_refunded", "supergroup_chat_created", "text", "unique_gift", @@ -1227,6 +1257,9 @@ def __init__( is_paid_post: Optional[bool] = None, direct_messages_topic: Optional[DirectMessagesTopic] = None, reply_to_checklist_task_id: Optional[int] = None, + suggested_post_declined: Optional["SuggestedPostDeclined"] = None, + suggested_post_paid: Optional["SuggestedPostPaid"] = None, + suggested_post_refunded: Optional["SuggestedPostRefunded"] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1345,6 +1378,9 @@ def __init__( self.is_paid_post: Optional[bool] = is_paid_post self.direct_messages_topic: Optional[DirectMessagesTopic] = direct_messages_topic self.reply_to_checklist_task_id: Optional[int] = reply_to_checklist_task_id + self.suggested_post_declined: Optional[SuggestedPostDeclined] = suggested_post_declined + self.suggested_post_paid: Optional[SuggestedPostPaid] = suggested_post_paid + self.suggested_post_refunded: Optional[SuggestedPostRefunded] = suggested_post_refunded self._effective_attachment = DEFAULT_NONE @@ -1499,6 +1535,11 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Message": ExternalReplyInfo, TextQuote, ) + from telegram._suggestedpost import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 + SuggestedPostDeclined, + SuggestedPostPaid, + SuggestedPostRefunded, + ) data["giveaway"] = de_json_optional(data.get("giveaway"), Giveaway, bot) data["giveaway_completed"] = de_json_optional( @@ -1534,6 +1575,15 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Message": data["direct_messages_topic"] = de_json_optional( data.get("direct_messages_topic"), DirectMessagesTopic, bot ) + data["suggested_post_declined"] = de_json_optional( + data.get("suggested_post_declined"), SuggestedPostDeclined, bot + ) + data["suggested_post_paid"] = de_json_optional( + data.get("suggested_post_paid"), SuggestedPostPaid, bot + ) + data["suggested_post_refunded"] = de_json_optional( + data.get("suggested_post_refunded"), SuggestedPostRefunded, bot + ) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility diff --git a/src/telegram/_suggestedpost.py b/src/telegram/_suggestedpost.py index fc597e3aad1..eb917b4eb66 100644 --- a/src/telegram/_suggestedpost.py +++ b/src/telegram/_suggestedpost.py @@ -21,6 +21,8 @@ import datetime as dtm from typing import TYPE_CHECKING, Literal, Optional +from telegram._message import Message +from telegram._payment.stars.staramount import StarAmount from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import de_json_optional from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp @@ -150,3 +152,208 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostP data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) return super().de_json(data=data, bot=bot) + + +class SuggestedPostDeclined(TelegramObject): + """ + Describes a service message about the rejection of a suggested post. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`suggested_post_message` and :attr:`comment` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + suggested_post_message (:class:`telegram.Message`, optional): + Message containing the suggested post. Note that the :class:`~telegram.Message` object + in this field will not contain the :attr:`~telegram.Message.reply_to_message` field + even if it itself is a reply. + comment (:obj:`str`, optional): + Comment with which the post was declined. + + Attributes: + suggested_post_message (:class:`telegram.Message`): + Optional. Message containing the suggested post. Note that the + :class:`~telegram.Message` object in this field will not contain + the :attr:`~telegram.Message.reply_to_message` field even if it itself is a reply. + comment (:obj:`str`): + Optional. Comment with which the post was declined. + + """ + + __slots__ = ("comment", "suggested_post_message") + + def __init__( + self, + suggested_post_message: Optional[Message] = None, + comment: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.suggested_post_message: Optional[Message] = suggested_post_message + self.comment: Optional[str] = comment + + self._id_attrs = (self.suggested_post_message, self.comment) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostDeclined": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["suggested_post_message"] = de_json_optional( + data.get("suggested_post_message"), Message, bot + ) + + return super().de_json(data=data, bot=bot) + + +class SuggestedPostPaid(TelegramObject): + """ + Describes a service message about a successful payment for a suggested post. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if all of their attributes are equal. + + .. versionadded:: NEXT.VERSION + + Args: + suggested_post_message (:class:`telegram.Message`, optional): + Message containing the suggested post. Note that the :class:`~telegram.Message` object + in this field will not contain the :attr:`~telegram.Message.reply_to_message` field + even if it itself is a reply. + currency (:obj:`str`): + Currency in which the payment was made. Currently, one of ``“XTR”`` for Telegram Stars + or ``“TON”`` for toncoins. + amount (:obj:`int`, optional): + The amount of the currency that was received by the channel in nanotoncoins; for + payments in toncoins only. + star_amount (:class:`telegram.StarAmount`, optional): + The amount of Telegram Stars that was received by the channel; for payments in Telegram + Stars only. + + + Attributes: + suggested_post_message (:class:`telegram.Message`): + Optional. Message containing the suggested post. Note that the + :class:`~telegram.Message` object in this field will not contain + the :attr:`~telegram.Message.reply_to_message` field even if it itself is a reply. + currency (:obj:`str`): + Currency in which the payment was made. Currently, one of ``“XTR”`` for Telegram Stars + or ``“TON”`` for toncoins. + amount (:obj:`int`): + Optional. The amount of the currency that was received by the channel in nanotoncoins; + for payments in toncoins only. + star_amount (:class:`telegram.StarAmount`): + Optional. The amount of Telegram Stars that was received by the channel; for payments + in Telegram Stars only. + + """ + + __slots__ = ("amount", "currency", "star_amount", "suggested_post_message") + + def __init__( + self, + currency: Literal["XTR", "TON"], + suggested_post_message: Optional[Message] = None, + amount: Optional[int] = None, + star_amount: Optional[StarAmount] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.currency: Literal["XTR", "TON"] = currency + # Optionals + self.suggested_post_message: Optional[Message] = suggested_post_message + self.amount: Optional[int] = amount + self.star_amount: Optional[StarAmount] = star_amount + + self._id_attrs = ( + self.currency, + self.suggested_post_message, + self.amount, + self.star_amount, + ) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostPaid": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["suggested_post_message"] = de_json_optional( + data.get("suggested_post_message"), Message, bot + ) + data["star_amount"] = de_json_optional(data.get("star_amount"), StarAmount, bot) + + return super().de_json(data=data, bot=bot) + + +class SuggestedPostRefunded(TelegramObject): + """ + Describes a service message about a payment refund for a suggested post. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`suggested_post_message` and :attr:`reason` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + suggested_post_message (:class:`telegram.Message`, optional): + Message containing the suggested post. Note that the :class:`~telegram.Message` object + in this field will not contain the :attr:`~telegram.Message.reply_to_message` field + even if it itself is a reply. + reason (:obj:`str`): + Reason for the refund. Currently, + one of :tg-const:`telegram.constants.SuggestedPostRefunded.POST_DELETED` if the post + was deleted within 24 hours of being posted or removed from scheduled messages without + being posted, or :tg-const:`telegram.constants.SuggestedPostRefunded.PAYMENT_REFUNDED` + if the payer refunded their payment. + + Attributes: + suggested_post_message (:class:`telegram.Message`): + Optional. Message containing the suggested post. Note that the + :class:`~telegram.Message` object in this field will not contain + the :attr:`~telegram.Message.reply_to_message` field even if it itself is a reply. + reason (:obj:`str`): + Reason for the refund. Currently, + one of :tg-const:`telegram.constants.SuggestedPostRefunded.POST_DELETED` if the post + was deleted within 24 hours of being posted or removed from scheduled messages without + being posted, or :tg-const:`telegram.constants.SuggestedPostRefunded.PAYMENT_REFUNDED` + if the payer refunded their payment. + + """ + + __slots__ = ("reason", "suggested_post_message") + + def __init__( + self, + reason: str, + suggested_post_message: Optional[Message] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.reason: str = reason + # Optionals + self.suggested_post_message: Optional[Message] = suggested_post_message + + self._id_attrs = (self.reason, self.suggested_post_message) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostRefunded": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["suggested_post_message"] = de_json_optional( + data.get("suggested_post_message"), Message, bot + ) + + return super().de_json(data=data, bot=bot) diff --git a/src/telegram/constants.py b/src/telegram/constants.py index 0fdd801c017..d200ca7bc15 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -118,6 +118,7 @@ "StoryAreaTypeType", "StoryLimit", "SuggestedPost", + "SuggestedPostRefunded", "TransactionPartnerType", "TransactionPartnerUser", "UniqueGiftInfoOrigin", @@ -2221,6 +2222,21 @@ class MessageType(StringEnum): .. versionadded:: v22.2 """ + SUGGESTED_POST_DECLINED = "suggested_post_declined" + """:obj:`str`: Messages with :attr:`telegram.Message.suggested_post_declined`. + + .. versionadded:: NEXT.VERSION + """ + SUGGESTED_POST_PAID = "suggested_post_paid" + """:obj:`str`: Messages with :attr:`telegram.Message.suggested_post_paid`. + + .. versionadded:: NEXT.VERSION + """ + SUGGESTED_POST_REFUNDED = "suggested_post_refunded" + """:obj:`str`: Messages with :attr:`telegram.Message.suggested_post_refunded`. + + .. versionadded:: NEXT.VERSION + """ PASSPORT_DATA = "passport_data" """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" PHOTO = "photo" @@ -3095,6 +3111,22 @@ class SuggestedPost(IntEnum): :class:`telegram.SuggestedPostParameters`.""" +class SuggestedPostRefunded(StringEnum): + """This enum contains available refund reasons for :class:`telegram.SuggestedPostRefunded`. + The enum members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + POST_DELETED = "post_deleted" + """:obj:`str`: The post was deleted within 24 hours of being posted or removed from + scheduled messages without being posted.""" + PAYMENT_REFUNDED = "payment_refunded" + """:obj:`str`: The payer refunded their payment.""" + + class TransactionPartnerType(StringEnum): """This enum contains the available types of :class:`telegram.TransactionPartner`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. diff --git a/src/telegram/ext/filters.py b/src/telegram/ext/filters.py index d385c45987b..91116afa392 100644 --- a/src/telegram/ext/filters.py +++ b/src/telegram/ext/filters.py @@ -1974,6 +1974,9 @@ def filter(self, update: Update) -> bool: or StatusUpdate.PINNED_MESSAGE.check_update(update) or StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update) or StatusUpdate.REFUNDED_PAYMENT.check_update(update) + or StatusUpdate.SUGGESTED_POST_DECLINED.check_update(update) + or StatusUpdate.SUGGESTED_POST_PAID.check_update(update) + or StatusUpdate.SUGGESTED_POST_REFUNDED.check_update(update) or StatusUpdate.UNIQUE_GIFT.check_update(update) or StatusUpdate.USERS_SHARED.check_update(update) or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update) @@ -2295,6 +2298,43 @@ def filter(self, message: Message) -> bool: .. versionadded:: 21.4 """ + class _SuggestedPostDeclined(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.suggested_post_declined) + + SUGGESTED_POST_DECLINED = _SuggestedPostDeclined( + "filters.StatusUpdate.SUGGESTED_POST_DECLINED" + ) + """Messages that contain :attr:`telegram.Message.suggested_post_declined`. + .. versionadded:: NEXT.VERSION + """ + + class _SuggestedPostPaid(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.suggested_post_paid) + + SUGGESTED_POST_PAID = _SuggestedPostPaid("filters.StatusUpdate.SUGGESTED_POST_PAID") + """Messages that contain :attr:`telegram.Message.suggested_post_paid`. + .. versionadded:: NEXT.VERSION + """ + + class _SuggestedPostRefunded(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.suggested_post_refunded) + + SUGGESTED_POST_REFUNDED = _SuggestedPostRefunded( + "filters.StatusUpdate.SUGGESTED_POST_REFUNDED" + ) + """Messages that contain :attr:`telegram.Message.suggested_post_refunded`. + .. versionadded:: NEXT.VERSION + """ + class _UniqueGift(MessageFilter): __slots__ = () diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index 01716e37f30..24504b72091 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -1101,6 +1101,21 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.REFUNDED_PAYMENT.check_update(update) update.message.refunded_payment = None + update.message.suggested_post_declined = "suggested_post_declined" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.SUGGESTED_POST_DECLINED.check_update(update) + update.message.suggested_post_declined = None + + update.message.suggested_post_paid = "suggested_post_paid" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.SUGGESTED_POST_PAID.check_update(update) + update.message.suggested_post_paid = None + + update.message.suggested_post_refunded = "suggested_post_refunded" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.SUGGESTED_POST_REFUNDED.check_update(update) + update.message.suggested_post_refunded = None + update.message.gift = "gift" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.GIFT.check_update(update) diff --git a/tests/test_message.py b/tests/test_message.py index 96dc063c120..24700e0b9fa 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -92,6 +92,7 @@ WebAppData, ) from telegram._directmessagestopic import DirectMessagesTopic +from telegram._suggestedpost import SuggestedPostDeclined, SuggestedPostPaid, SuggestedPostRefunded from telegram._utils.datetime import UTC from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ODVInput @@ -364,6 +365,31 @@ def message(bot): ) }, {"reply_to_checklist_task_id": 11}, + { + "suggested_post_declined": SuggestedPostDeclined( + suggested_post_message=Message( + 7, dtm.datetime.utcnow(), Chat(13, "channel"), User(9, "i", False) + ), + comment="comment", + ) + }, + { + "suggested_post_paid": SuggestedPostPaid( + currency="XTR", + suggested_post_message=Message( + 7, dtm.datetime.utcnow(), Chat(13, "channel"), User(9, "i", False) + ), + amount=100, + ) + }, + { + "suggested_post_refunded": SuggestedPostRefunded( + reason="post_deleted", + suggested_post_message=Message( + 7, dtm.datetime.utcnow(), Chat(13, "channel"), User(9, "i", False) + ), + ) + }, ], ids=[ "reply", @@ -448,6 +474,9 @@ def message(bot): "is_paid_post", "direct_messages_topic", "reply_to_checklist_task_id", + "suggested_post_declined", + "suggested_post_paid", + "suggested_post_refunded", ], ) def message_params(bot, request): diff --git a/tests/test_suggestedpost.py b/tests/test_suggestedpost.py index 73fa9206779..625c5f85d12 100644 --- a/tests/test_suggestedpost.py +++ b/tests/test_suggestedpost.py @@ -22,7 +22,16 @@ import pytest from telegram import Dice -from telegram._suggestedpost import SuggestedPostParameters, SuggestedPostPrice +from telegram._chat import Chat +from telegram._message import Message +from telegram._payment.stars.staramount import StarAmount +from telegram._suggestedpost import ( + SuggestedPostDeclined, + SuggestedPostPaid, + SuggestedPostParameters, + SuggestedPostPrice, + SuggestedPostRefunded, +) from telegram._utils.datetime import UTC, to_timestamp from tests.auxil.slots import mro_slots @@ -157,3 +166,195 @@ def test_equality(self, suggested_post_price): assert a != e assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def suggested_post_declined(): + return SuggestedPostDeclined( + suggested_post_message=SuggestedPostDeclinedTestBase.suggested_post_message, + comment=SuggestedPostDeclinedTestBase.comment, + ) + + +class SuggestedPostDeclinedTestBase: + suggested_post_message = Message(1, dtm.datetime.now(), Chat(1, ""), text="post this pls.") + comment = "another time" + + +class TestSuggestedPostDeclinedWithoutRequest(SuggestedPostDeclinedTestBase): + def test_slot_behaviour(self, suggested_post_declined): + for attr in suggested_post_declined.__slots__: + assert getattr(suggested_post_declined, attr, "err") != "err", ( + f"got extra slot '{attr}'" + ) + assert len(mro_slots(suggested_post_declined)) == len( + set(mro_slots(suggested_post_declined)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "suggested_post_message": self.suggested_post_message.to_dict(), + "comment": self.comment, + } + spd = SuggestedPostDeclined.de_json(json_dict, offline_bot) + assert spd.suggested_post_message == self.suggested_post_message + assert spd.comment == self.comment + assert spd.api_kwargs == {} + + def test_to_dict(self, suggested_post_declined): + spd_dict = suggested_post_declined.to_dict() + + assert isinstance(spd_dict, dict) + assert spd_dict["suggested_post_message"] == self.suggested_post_message.to_dict() + assert spd_dict["comment"] == self.comment + + def test_equality(self, suggested_post_declined): + a = suggested_post_declined + b = SuggestedPostDeclined( + suggested_post_message=self.suggested_post_message, comment=self.comment + ) + c = SuggestedPostDeclined(suggested_post_message=self.suggested_post_message, comment="no") + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def suggested_post_paid(): + return SuggestedPostPaid( + currency=SuggestedPostPaidTestBase.currency, + suggested_post_message=SuggestedPostPaidTestBase.suggested_post_message, + amount=SuggestedPostPaidTestBase.amount, + star_amount=SuggestedPostPaidTestBase.star_amount, + ) + + +class SuggestedPostPaidTestBase: + suggested_post_message = Message(1, dtm.datetime.now(), Chat(1, ""), text="post this pls.") + currency = "XTR" + amount = 100 + star_amount = StarAmount(100) + + +class TestSuggestedPostPaidWithoutRequest(SuggestedPostPaidTestBase): + def test_slot_behaviour(self, suggested_post_paid): + for attr in suggested_post_paid.__slots__: + assert getattr(suggested_post_paid, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(suggested_post_paid)) == len(set(mro_slots(suggested_post_paid))), ( + "duplicate slot" + ) + + def test_de_json(self, offline_bot): + json_dict = { + "suggested_post_message": self.suggested_post_message.to_dict(), + "currency": self.currency, + "amount": self.amount, + "star_amount": self.star_amount.to_dict(), + } + spp = SuggestedPostPaid.de_json(json_dict, offline_bot) + assert spp.suggested_post_message == self.suggested_post_message + assert spp.currency == self.currency + assert spp.amount == self.amount + assert spp.star_amount == self.star_amount + assert spp.api_kwargs == {} + + def test_to_dict(self, suggested_post_paid): + spp_dict = suggested_post_paid.to_dict() + + assert isinstance(spp_dict, dict) + assert spp_dict["suggested_post_message"] == self.suggested_post_message.to_dict() + assert spp_dict["currency"] == self.currency + assert spp_dict["amount"] == self.amount + assert spp_dict["star_amount"] == self.star_amount.to_dict() + + def test_equality(self, suggested_post_paid): + a = suggested_post_paid + b = SuggestedPostPaid( + suggested_post_message=self.suggested_post_message, + currency=self.currency, + amount=self.amount, + star_amount=self.star_amount, + ) + c = SuggestedPostPaid( + suggested_post_message=self.suggested_post_message, + currency=self.currency, + amount=self.amount - 1, + star_amount=StarAmount(self.amount - 1), + ) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def suggested_post_refunded(): + return SuggestedPostRefunded( + reason=SuggestedPostRefundedTestBase.reason, + suggested_post_message=SuggestedPostRefundedTestBase.suggested_post_message, + ) + + +class SuggestedPostRefundedTestBase: + reason = "post_deleted" + suggested_post_message = Message(1, dtm.datetime.now(), Chat(1, ""), text="post this pls.") + + +class TestSuggestedPostRefundedWithoutRequest(SuggestedPostRefundedTestBase): + def test_slot_behaviour(self, suggested_post_refunded): + for attr in suggested_post_refunded.__slots__: + assert getattr(suggested_post_refunded, attr, "err") != "err", ( + f"got extra slot '{attr}'" + ) + assert len(mro_slots(suggested_post_refunded)) == len( + set(mro_slots(suggested_post_refunded)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "suggested_post_message": self.suggested_post_message.to_dict(), + "reason": self.reason, + } + spr = SuggestedPostRefunded.de_json(json_dict, offline_bot) + assert spr.suggested_post_message == self.suggested_post_message + assert spr.reason == self.reason + assert spr.api_kwargs == {} + + def test_to_dict(self, suggested_post_refunded): + spr_dict = suggested_post_refunded.to_dict() + + assert isinstance(spr_dict, dict) + assert spr_dict["suggested_post_message"] == self.suggested_post_message.to_dict() + assert spr_dict["reason"] == self.reason + + def test_equality(self, suggested_post_refunded): + a = suggested_post_refunded + b = SuggestedPostRefunded( + suggested_post_message=self.suggested_post_message, reason=self.reason + ) + c = SuggestedPostRefunded( + suggested_post_message=self.suggested_post_message, reason="payment_refunded" + ) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != e + assert hash(a) != hash(e) From d5f8c34dd0bae4fdf7a487b0afb3a8a0d81449ad Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 7 Sep 2025 14:46:39 -0400 Subject: [PATCH 12/17] API 9.2 Suggested Post Methods (#4916) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .../4911.kiF45Y4cfPGMq5cuLpa5da.toml | 1 + docs/source/inclusions/bot_methods.rst | 4 + src/telegram/_bot.py | 107 +++++++++++++++++- src/telegram/_chat.py | 70 ++++++++++++ src/telegram/_message.py | 74 ++++++++++++ src/telegram/constants.py | 6 +- src/telegram/ext/_extbot.py | 50 ++++++++ tests/test_bot.py | 45 ++++++++ tests/test_chat.py | 38 +++++++ tests/test_message.py | 50 ++++++++ 10 files changed, 440 insertions(+), 5 deletions(-) diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index 36f013cff7c..154012d2e61 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -5,6 +5,7 @@ pull_requests = [ { uid = "4918", author_uid = "Poolitzer" }, { uid = "4917", author_uid = "Poolitzer" }, { uid = "4914", author_uid = "harshil21"}, + { uid = "4916", author_uid = "harshil21"}, { uid = "4912", author_uid = "aelkheir" }, { uid = "4921", author_uid = "aelkheir" }, ] diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 470fcb17ff6..128fa26ac5c 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -121,6 +121,10 @@ - Used for approving a chat join request * - :meth:`~telegram.Bot.decline_chat_join_request` - Used for declining a chat join request + * - :meth:`~telegram.Bot.approve_suggested_post` + - Used for approving a suggested post + * - :meth:`~telegram.Bot.decline_suggested_post` + - Used for declining a suggested post * - :meth:`~telegram.Bot.ban_chat_member` - Used for banning a member from the chat * - :meth:`~telegram.Bot.unban_chat_member` diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 81da34c35bd..54bdfcb701c 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -11365,8 +11365,6 @@ async def remove_chat_verification( """Removes verification from a chat that is currently verified |org-verify| represented by the bot. - - .. versionadded:: 21.10 Args: @@ -11404,8 +11402,6 @@ async def remove_user_verification( """Removes verification from a user who is currently verified |org-verify| represented by the bot. - - .. versionadded:: 21.10 Args: @@ -11460,6 +11456,105 @@ async def get_my_star_balance( ) ) + async def approve_suggested_post( + self, + chat_id: int, + message_id: int, + send_date: Optional[Union[int, dtm.datetime]] = 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: + """ + Use this method to approve a suggested post in a direct messages chat. + The bot must have the :attr:`~telegram.ChatMemberAdministrator.can_post_messages` + administrator right in the corresponding channel chat. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int`): Unique identifier of the target direct messages chat. + message_id (:obj:`int`): Identifier of a suggested post message to approve. + send_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the post is + expected to be published; omit if the date has already been specified when the + suggested post was created. If specified, then the date must be not more than + :tg-const:`telegram.constants.SuggestedPost.MAX_SEND_DATE` seconds (30 days) + in the future. + + |tz-naive-dtms| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "send_date": send_date, + } + + return await self._post( + "approveSuggestedPost", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def decline_suggested_post( + self, + chat_id: int, + message_id: int, + comment: 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: + """ + Use this method to decline a suggested post in a direct messages chat. + The bot must have the :attr:`~telegram.ChatMemberAdministrator.can_manage_direct_messages` + administrator right in the corresponding channel chat. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int`): Unique identifier of the target direct messages chat. + message_id (:obj:`int`): Identifier of a suggested post message to decline. + comment (:obj:`str`, optional): Comment for the creator of the suggested post. + 0-:tg-const:`telegram.constants.SuggestedPost.MAX_COMMENT_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "comment": comment, + } + + return await self._post( + "declineSuggestedPost", + 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: # noqa: ARG002 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -11780,3 +11875,7 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`remove_user_verification`""" getMyStarBalance = get_my_star_balance """Alias for :meth:`get_my_star_balance`""" + approveSuggestedPost = approve_suggested_post + """Alias for :meth:`approve_suggested_post`""" + declineSuggestedPost = decline_suggested_post + """Alias for :meth:`decline_suggested_post`""" diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index d360369a037..77eb2d9a386 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -3780,6 +3780,76 @@ async def read_business_message( api_kwargs=api_kwargs, ) + async def approve_suggested_post( + self, + message_id: int, + send_date: Optional[Union[int, dtm.datetime]] = 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: + """ + Shortcut for:: + + await bot.approve_suggested_post(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_suggested_post`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().approve_suggested_post( + chat_id=self.id, + message_id=message_id, + send_date=send_date, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def decline_suggested_post( + self, + message_id: int, + comment: 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: + """ + Shortcut for:: + + await bot.decline_suggested_post(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_suggested_post`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().decline_suggested_post( + chat_id=self.id, + message_id=message_id, + comment=comment, + 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/src/telegram/_message.py b/src/telegram/_message.py index d7ca62fbf60..2c72d22adea 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -4890,6 +4890,80 @@ async def read_business_message( api_kwargs=api_kwargs, ) + async def approve_suggested_post( + self, + send_date: Optional[Union[int, dtm.datetime]] = 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: + """Shortcut for:: + + await bot.approve_suggested_post( + chat_id=message.chat_id, + message_id=message.message_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_suggested_post`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + """ + return await self.get_bot().approve_suggested_post( + chat_id=self.chat_id, + message_id=self.message_id, + send_date=send_date, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def decline_suggested_post( + self, + comment: 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: + """Shortcut for:: + + await bot.decline_suggested_post( + chat_id=message.chat_id, + message_id=message.message_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_suggested_post`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + """ + return await self.get_bot().decline_suggested_post( + chat_id=self.chat_id, + message_id=self.message_id, + comment=comment, + 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/src/telegram/constants.py b/src/telegram/constants.py index d200ca7bc15..0857d95772c 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -3073,7 +3073,7 @@ class StoryLimit(StringEnum): class SuggestedPost(IntEnum): """This enum contains limitations for :class:`telegram.SuggestedPostPrice`\ -/:class:`telegram.SuggestedPostParameters`. The enum +/:class:`telegram.SuggestedPostParameters`/:meth:`telegram.Bot.decline_suggested_post`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. .. versionadded:: NEXT.VERSION @@ -3109,6 +3109,10 @@ class SuggestedPost(IntEnum): """:obj:`int`: Maximum number of seconds in the future for the :paramref:`~telegram.SuggestedPostParameters.send_date` parameter of :class:`telegram.SuggestedPostParameters`.""" + MAX_COMMENT_LENGTH = 128 + """:obj:`int`: Maximum number of characters in the + :paramref:`telegram.Bot.decline_suggested_post.comment` parameter. + """ class SuggestedPostRefunded(StringEnum): diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index 0d543f6a830..65b6e1f2fa4 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -5222,6 +5222,54 @@ async def get_my_star_balance( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def decline_suggested_post( + self, + chat_id: int, + message_id: int, + comment: 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().decline_suggested_post( + chat_id=chat_id, + message_id=message_id, + comment=comment, + 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 approve_suggested_post( + self, + chat_id: int, + message_id: int, + send_date: Optional[Union[int, dtm.datetime]] = 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().approve_suggested_post( + chat_id=chat_id, + message_id=message_id, + send_date=send_date, + 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 @@ -5378,3 +5426,5 @@ async def get_my_star_balance( removeChatVerification = remove_chat_verification removeUserVerification = remove_user_verification getMyStarBalance = get_my_star_balance + approveSuggestedPost = approve_suggested_post + declineSuggestedPost = decline_suggested_post diff --git a/tests/test_bot.py b/tests/test_bot.py index 00026b8f70e..66c77316abb 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2614,6 +2614,51 @@ async def do_request(url, request_data: RequestData, *args, **kwargs): obj = await offline_bot.get_my_star_balance() assert isinstance(obj, StarAmount) + async def test_approve_suggested_post(self, offline_bot, monkeypatch): + "No way to test this without receiving suggested posts" + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.json_parameters + chat_id = data.get("chat_id") == "1234" + message_id = data.get("message_id") == "5678" + send_date = data.get("send_date", "1577887200") == "1577887200" + return chat_id and message_id and send_date + + until = from_timestamp(1577887200) + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + + assert await offline_bot.approve_suggested_post(1234, 5678, 1577887200) + assert await offline_bot.approve_suggested_post(1234, 5678, until) + + async def test_approve_suggested_post_with_tz(self, monkeypatch, tz_bot): + until = dtm.datetime(2020, 1, 11, 16, 13) + until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo) + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + chat_id = data["chat_id"] == 2 + message_id = data["message_id"] == 32 + until_date = data.get("until_date", until_timestamp) == until_timestamp + return chat_id and message_id and until_date + + monkeypatch.setattr(tz_bot.request, "post", make_assertion) + + assert await tz_bot.approve_suggested_post(2, 32) + assert await tz_bot.approve_suggested_post(2, 32, send_date=until) + assert await tz_bot.approve_suggested_post(2, 32, send_date=until_timestamp) + + async def test_decline_suggested_post(self, offline_bot, monkeypatch): + "No way to test this without receiving suggested posts" + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("chat_id") == 1234 + assert request_data.parameters.get("message_id") == 5678 + assert request_data.parameters.get("comment") == "declined" + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + + await offline_bot.decline_suggested_post(1234, 5678, "declined") + class TestBotWithRequest: """ diff --git a/tests/test_chat.py b/tests/test_chat.py index d4b98c54dd5..7ab0c2f1d0f 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1457,6 +1457,44 @@ async def make_assertion(*_, **kwargs): message_id="message_id", business_connection_id="business_connection_id" ) + async def test_instance_method_approve_suggested_post(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["message_id"] == "message_id" + and kwargs["send_date"] == "send_date" + ) + + assert check_shortcut_signature( + Chat.approve_suggested_post, Bot.approve_suggested_post, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.approve_suggested_post, chat.get_bot(), "approve_suggested_post" + ) + assert await check_defaults_handling(chat.approve_suggested_post, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "approve_suggested_post", make_assertion) + assert await chat.approve_suggested_post(message_id="message_id", send_date="send_date") + + async def test_instance_method_decline_suggested_post(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["message_id"] == "message_id" + and kwargs["comment"] == "comment" + ) + + assert check_shortcut_signature( + Chat.decline_suggested_post, Bot.decline_suggested_post, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.decline_suggested_post, chat.get_bot(), "decline_suggested_post" + ) + assert await check_defaults_handling(chat.decline_suggested_post, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "decline_suggested_post", make_assertion) + assert await chat.decline_suggested_post(message_id="message_id", comment="comment") + 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 24700e0b9fa..e7554d416f9 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -3214,3 +3214,53 @@ def test_attachement_successful_payment_deprecated(self, message, recwarn): ) assert recwarn[0].category is PTBDeprecationWarning assert recwarn[0].filename == __file__ + + async def test_approve_suggested_post(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_id"] == message.message_id + and kwargs["send_date"] == 1234567890 + ) + + assert check_shortcut_signature( + Message.approve_suggested_post, + Bot.approve_suggested_post, + ["chat_id", "message_id"], + [], + ) + assert await check_shortcut_call( + message.approve_suggested_post, + message.get_bot(), + "approve_suggested_post", + shortcut_kwargs=["chat_id", "message_id"], + ) + assert await check_defaults_handling(message.approve_suggested_post, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "approve_suggested_post", make_assertion) + assert await message.approve_suggested_post(send_date=1234567890) + + async def test_decline_suggested_post(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_id"] == message.message_id + and kwargs["comment"] == "some comment" + ) + + assert check_shortcut_signature( + Message.decline_suggested_post, + Bot.decline_suggested_post, + ["chat_id", "message_id"], + [], + ) + assert await check_shortcut_call( + message.decline_suggested_post, + message.get_bot(), + "decline_suggested_post", + shortcut_kwargs=["chat_id", "message_id"], + ) + assert await check_defaults_handling(message.decline_suggested_post, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "decline_suggested_post", make_assertion) + assert await message.decline_suggested_post(comment="some comment") From de8017277dda550327b9cf45555dbc0b03d07501 Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 7 Sep 2025 23:02:57 +0300 Subject: [PATCH 13/17] API 9.2 `SuggestedPost[Info,Approved,ApprovalFailed]` (#4931) --- .../4911.kiF45Y4cfPGMq5cuLpa5da.toml | 1 + docs/source/telegram.at-tree.rst | 3 + .../telegram.suggestedpostapprovalfailed.rst | 6 + .../source/telegram.suggestedpostapproved.rst | 6 + docs/source/telegram.suggestedpostinfo.rst | 6 + src/telegram/__init__.py | 6 + src/telegram/_message.py | 56 ++++ src/telegram/_suggestedpost.py | 216 +++++++++++++++- src/telegram/constants.py | 35 +++ src/telegram/ext/filters.py | 43 ++++ tests/ext/test_filters.py | 15 ++ tests/test_message.py | 35 ++- tests/test_suggestedpost.py | 240 ++++++++++++++++++ 13 files changed, 666 insertions(+), 2 deletions(-) create mode 100644 docs/source/telegram.suggestedpostapprovalfailed.rst create mode 100644 docs/source/telegram.suggestedpostapproved.rst create mode 100644 docs/source/telegram.suggestedpostinfo.rst diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index 154012d2e61..105a27270f5 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -8,4 +8,5 @@ pull_requests = [ { uid = "4916", author_uid = "harshil21"}, { uid = "4912", author_uid = "aelkheir" }, { uid = "4921", author_uid = "aelkheir" }, + { uid = "4931", author_uid = "aelkheir" }, ] diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 9343034d9a8..f04e35df648 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -169,7 +169,10 @@ Available Types telegram.storyareatypesuggestedreaction telegram.storyareatypeuniquegift telegram.storyareatypeweather + telegram.suggestedpostapprovalfailed + telegram.suggestedpostapproved telegram.suggestedpostdeclined + telegram.suggestedpostinfo telegram.suggestedpostpaid telegram.suggestedpostparameters telegram.suggestedpostprice diff --git a/docs/source/telegram.suggestedpostapprovalfailed.rst b/docs/source/telegram.suggestedpostapprovalfailed.rst new file mode 100644 index 00000000000..5b730f18583 --- /dev/null +++ b/docs/source/telegram.suggestedpostapprovalfailed.rst @@ -0,0 +1,6 @@ +SuggestedPostApprovalFailed +=========================== + +.. autoclass:: telegram.SuggestedPostApprovalFailed + :members: + :show-inheritance: diff --git a/docs/source/telegram.suggestedpostapproved.rst b/docs/source/telegram.suggestedpostapproved.rst new file mode 100644 index 00000000000..c9e74a94652 --- /dev/null +++ b/docs/source/telegram.suggestedpostapproved.rst @@ -0,0 +1,6 @@ +SuggestedPostApproved +===================== + +.. autoclass:: telegram.SuggestedPostApproved + :members: + :show-inheritance: diff --git a/docs/source/telegram.suggestedpostinfo.rst b/docs/source/telegram.suggestedpostinfo.rst new file mode 100644 index 00000000000..a974dda9887 --- /dev/null +++ b/docs/source/telegram.suggestedpostinfo.rst @@ -0,0 +1,6 @@ +SuggestedPostInfo +================= + +.. autoclass:: telegram.SuggestedPostInfo + :members: + :show-inheritance: diff --git a/src/telegram/__init__.py b/src/telegram/__init__.py index 4bf21bcc4da..0d77c81eeba 100644 --- a/src/telegram/__init__.py +++ b/src/telegram/__init__.py @@ -265,7 +265,10 @@ "StoryAreaTypeUniqueGift", "StoryAreaTypeWeather", "SuccessfulPayment", + "SuggestedPostApprovalFailed", + "SuggestedPostApproved", "SuggestedPostDeclined", + "SuggestedPostInfo", "SuggestedPostPaid", "SuggestedPostParameters", "SuggestedPostPrice", @@ -579,7 +582,10 @@ StoryAreaTypeWeather, ) from ._suggestedpost import ( + SuggestedPostApprovalFailed, + SuggestedPostApproved, SuggestedPostDeclined, + SuggestedPostInfo, SuggestedPostPaid, SuggestedPostParameters, SuggestedPostPrice, diff --git a/src/telegram/_message.py b/src/telegram/_message.py index f512a59dc62..5c81a05367a 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -119,7 +119,10 @@ MessageId, MessageOrigin, ReactionType, + SuggestedPostApprovalFailed, + SuggestedPostApproved, SuggestedPostDeclined, + SuggestedPostInfo, SuggestedPostPaid, SuggestedPostParameters, SuggestedPostRefunded, @@ -347,6 +350,13 @@ class Message(MaybeInaccessibleMessage): .. versionadded:: 20.8 + suggested_post_info (:class:`telegram.SuggestedPostInfo`, optional): Information about + suggested post parameters if the message is a suggested post in a channel direct + messages chat. If the message is an approved or declined suggested post, then it can't + be edited. + + .. versionadded:: NEXT.VERSION + effect_id (:obj:`str`, optional): Unique identifier of the message effect added to the message. @@ -571,6 +581,14 @@ class Message(MaybeInaccessibleMessage): message: the price for paid messages has changed in the chat .. versionadded:: 22.1 + suggested_post_approved (:class:`telegram.SuggestedPostApproved`, optional): Service + message: a suggested post was approved. + + .. versionadded:: NEXT.VERSION + suggested_post_approval_failed (:class:`telegram.SuggestedPostApproved`, optional): Service + message: approval of a suggested post has failed. + + .. versionadded:: NEXT.VERSION suggested_post_declined (:class:`telegram.SuggestedPostDeclined`, optional): Service message: a suggested post was declined. @@ -716,6 +734,13 @@ class Message(MaybeInaccessibleMessage): .. versionadded:: 20.8 + suggested_post_info (:class:`telegram.SuggestedPostInfo`): Optional. Information about + suggested post parameters if the message is a suggested post in a channel direct + messages chat. If the message is an approved or declined suggested post, then it can't + be edited. + + .. versionadded:: NEXT.VERSION + effect_id (:obj:`str`): Optional. Unique identifier of the message effect added to the message. @@ -955,6 +980,14 @@ class Message(MaybeInaccessibleMessage): message: the price for paid messages has changed in the chat .. versionadded:: 22.1 + suggested_post_approved (:class:`telegram.SuggestedPostApproved`): Optional. Service + message: a suggested post was approved. + + .. versionadded:: NEXT.VERSION + suggested_post_approval_failed (:class:`telegram.SuggestedPostApproved`): Optional. Service + message: approval of a suggested post has failed. + + .. versionadded:: NEXT.VERSION suggested_post_declined (:class:`telegram.SuggestedPostDeclined`): Optional. Service message: a suggested post was declined. @@ -1139,7 +1172,10 @@ class Message(MaybeInaccessibleMessage): "sticker", "story", "successful_payment", + "suggested_post_approval_failed", + "suggested_post_approved", "suggested_post_declined", + "suggested_post_info", "suggested_post_paid", "suggested_post_refunded", "supergroup_chat_created", @@ -1260,6 +1296,9 @@ def __init__( suggested_post_declined: Optional["SuggestedPostDeclined"] = None, suggested_post_paid: Optional["SuggestedPostPaid"] = None, suggested_post_refunded: Optional["SuggestedPostRefunded"] = None, + suggested_post_info: Optional["SuggestedPostInfo"] = None, + suggested_post_approved: Optional["SuggestedPostApproved"] = None, + suggested_post_approval_failed: Optional["SuggestedPostApprovalFailed"] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1381,6 +1420,11 @@ def __init__( self.suggested_post_declined: Optional[SuggestedPostDeclined] = suggested_post_declined self.suggested_post_paid: Optional[SuggestedPostPaid] = suggested_post_paid self.suggested_post_refunded: Optional[SuggestedPostRefunded] = suggested_post_refunded + self.suggested_post_info: Optional[SuggestedPostInfo] = suggested_post_info + self.suggested_post_approved: Optional[SuggestedPostApproved] = suggested_post_approved + self.suggested_post_approval_failed: Optional[SuggestedPostApprovalFailed] = ( + suggested_post_approval_failed + ) self._effective_attachment = DEFAULT_NONE @@ -1536,7 +1580,10 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Message": TextQuote, ) from telegram._suggestedpost import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 + SuggestedPostApprovalFailed, + SuggestedPostApproved, SuggestedPostDeclined, + SuggestedPostInfo, SuggestedPostPaid, SuggestedPostRefunded, ) @@ -1584,6 +1631,15 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Message": data["suggested_post_refunded"] = de_json_optional( data.get("suggested_post_refunded"), SuggestedPostRefunded, bot ) + data["suggested_post_info"] = de_json_optional( + data.get("suggested_post_info"), SuggestedPostInfo, bot + ) + data["suggested_post_approved"] = de_json_optional( + data.get("suggested_post_approved"), SuggestedPostApproved, bot + ) + data["suggested_post_approval_failed"] = de_json_optional( + data.get("suggested_post_approval_failed"), SuggestedPostApprovalFailed, bot + ) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility diff --git a/src/telegram/_suggestedpost.py b/src/telegram/_suggestedpost.py index eb917b4eb66..a6be1407ccb 100644 --- a/src/telegram/_suggestedpost.py +++ b/src/telegram/_suggestedpost.py @@ -19,11 +19,13 @@ """This module contains objects related to Telegram suggested posts.""" import datetime as dtm -from typing import TYPE_CHECKING, Literal, Optional +from typing import TYPE_CHECKING, Final, Literal, Optional +from telegram import constants from telegram._message import Message from telegram._payment.stars.staramount import StarAmount from telegram._telegramobject import TelegramObject +from telegram._utils import enum from telegram._utils.argumentparsing import de_json_optional from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict @@ -154,6 +156,87 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostP return super().de_json(data=data, bot=bot) +class SuggestedPostInfo(TelegramObject): + """ + Contains information about a suggested post. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`state` and :attr:`price` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + state (:obj:`str`): + State of the suggested post. Currently, it can be one of + :tg-const:`~telegram.constants.SuggestedPostInfoState.PENDING`, + :tg-const:`~telegram.constants.SuggestedPostInfoState.APPROVED`, + :tg-const:`~telegram.constants.SuggestedPostInfoState.DECLINED`. + price (:obj:`SuggestedPostPrice`, optional): + Proposed price of the post. If the field is omitted, then the post is unpaid. + send_date (:class:`datetime.datetime`, optional): + Proposed send date of the post. If the field is omitted, then the post can be published + at any time within 30 days at the sole discretion of the user or administrator who + approves it. + |datetime_localization| + + Attributes: + state (:obj:`str`): + State of the suggested post. Currently, it can be one of + :tg-const:`~telegram.constants.SuggestedPostInfoState.PENDING`, + :tg-const:`~telegram.constants.SuggestedPostInfoState.APPROVED`, + :tg-const:`~telegram.constants.SuggestedPostInfoState.DECLINED`. + price (:obj:`SuggestedPostPrice`): + Optional. Proposed price of the post. If the field is omitted, then the post is unpaid. + send_date (:class:`datetime.datetime`): + Optional. Proposed send date of the post. If the field is omitted, then the post can be + published at any time within 30 days at the sole discretion of the user or + administrator who approves it. + |datetime_localization| + + """ + + __slots__ = ("price", "send_date", "state") + + PENDING: Final[str] = constants.SuggestedPostInfoState.PENDING + """:const:`telegram.constants.SuggestedPostInfoState.PENDING`""" + APPROVED: Final[str] = constants.SuggestedPostInfoState.APPROVED + """:const:`telegram.constants.SuggestedPostInfoState.APPROVED`""" + DECLINED: Final[str] = constants.SuggestedPostInfoState.DECLINED + """:const:`telegram.constants.SuggestedPostInfoState.DECLINED`""" + + def __init__( + self, + state: Literal["pending", "approved", "declined"], + price: Optional[SuggestedPostPrice] = None, + send_date: Optional[dtm.datetime] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.state: str = enum.get_member(constants.SuggestedPostInfoState, state, state) + # Optionals + self.price: Optional[SuggestedPostPrice] = price + self.send_date: Optional[dtm.datetime] = send_date + + self._id_attrs = (self.state, self.price) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostInfo": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + + data["price"] = de_json_optional(data.get("price"), SuggestedPostPrice, bot) + data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) + + return super().de_json(data=data, bot=bot) + + class SuggestedPostDeclined(TelegramObject): """ Describes a service message about the rejection of a suggested post. @@ -357,3 +440,134 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostR ) return super().de_json(data=data, bot=bot) + + +class SuggestedPostApproved(TelegramObject): + """ + Describes a service message about the approval of a suggested post. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if all of their attributes are equal. + + .. versionadded:: NEXT.VERSION + + Args: + suggested_post_message (:class:`telegram.Message`, optional): + Message containing the suggested post. Note that the :class:`~telegram.Message` object + in this field will not contain the :attr:`~telegram.Message.reply_to_message` field + even if it itself is a reply. + price (:obj:`SuggestedPostPrice`, optional): + Amount paid for the post. + send_date (:class:`datetime.datetime`): + Date when the post will be published. + |datetime_localization| + + Attributes: + suggested_post_message (:class:`telegram.Message`): + Optional. Message containing the suggested post. Note that the + :class:`~telegram.Message` object in this field will not contain + the :attr:`~telegram.Message.reply_to_message` field even if it itself is a reply. + price (:obj:`SuggestedPostPrice`): + Optional. Amount paid for the post. + send_date (:class:`datetime.datetime`): + Date when the post will be published. + |datetime_localization| + + """ + + __slots__ = ("price", "send_date", "suggested_post_message") + + def __init__( + self, + send_date: dtm.datetime, + suggested_post_message: Optional[Message] = None, + price: Optional[SuggestedPostPrice] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.send_date: dtm.datetime = send_date + # Optionals + self.suggested_post_message: Optional[Message] = suggested_post_message + self.price: Optional[SuggestedPostPrice] = price + + self._id_attrs = (self.send_date, self.suggested_post_message, self.price) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostApproved": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + + data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) + data["price"] = de_json_optional(data.get("price"), SuggestedPostPrice, bot) + data["suggested_post_message"] = de_json_optional( + data.get("suggested_post_message"), Message, bot + ) + + return super().de_json(data=data, bot=bot) + + +class SuggestedPostApprovalFailed(TelegramObject): + """ + Describes a service message about the failed approval of a suggested post. Currently, only + caused by insufficient user funds at the time of approval. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`suggested_post_message` and :attr:`price` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + suggested_post_message (:class:`telegram.Message`, optional): + Message containing the suggested post. Note that the :class:`~telegram.Message` object + in this field will not contain the :attr:`~telegram.Message.reply_to_message` field + even if it itself is a reply. + price (:obj:`SuggestedPostPrice`): + Expected price of the post. + + Attributes: + suggested_post_message (:class:`telegram.Message`): + Optional. Message containing the suggested post. Note that the + :class:`~telegram.Message` object in this field will not contain + the :attr:`~telegram.Message.reply_to_message` field even if it itself is a reply. + price (:obj:`SuggestedPostPrice`): + Expected price of the post. + + """ + + __slots__ = ("price", "suggested_post_message") + + def __init__( + self, + price: SuggestedPostPrice, + suggested_post_message: Optional[Message] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.price: SuggestedPostPrice = price + # Optionals + self.suggested_post_message: Optional[Message] = suggested_post_message + + self._id_attrs = (self.price, self.suggested_post_message) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostApprovalFailed": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["price"] = de_json_optional(data.get("price"), SuggestedPostPrice, bot) + data["suggested_post_message"] = de_json_optional( + data.get("suggested_post_message"), Message, bot + ) + + return super().de_json(data=data, bot=bot) diff --git a/src/telegram/constants.py b/src/telegram/constants.py index 0857d95772c..9641643028c 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -118,6 +118,7 @@ "StoryAreaTypeType", "StoryLimit", "SuggestedPost", + "SuggestedPostInfoState", "SuggestedPostRefunded", "TransactionPartnerType", "TransactionPartnerUser", @@ -2222,9 +2223,24 @@ class MessageType(StringEnum): .. versionadded:: v22.2 """ + SUGGESTED_POST_APPROVAL_FAILED = "suggested_post_approval_failed" + """:obj:`str`: Messages with :attr:`telegram.Message.suggested_post_approval_failed`. + + .. versionadded:: NEXT.VERSION + """ + SUGGESTED_POST_APPROVED = "suggested_post_approved" + """:obj:`str`: Messages with :attr:`telegram.Message.suggested_post_approved`. + + .. versionadded:: NEXT.VERSION + """ SUGGESTED_POST_DECLINED = "suggested_post_declined" """:obj:`str`: Messages with :attr:`telegram.Message.suggested_post_declined`. + .. versionadded:: NEXT.VERSION + """ + SUGGESTED_POST_INFO = "suggested_post_info" + """:obj:`str`: Messages with :attr:`telegram.Message.suggested_post_info`. + .. versionadded:: NEXT.VERSION """ SUGGESTED_POST_PAID = "suggested_post_paid" @@ -3605,6 +3621,25 @@ class ForumTopicLimit(IntEnum): """ +class SuggestedPostInfoState(StringEnum): + """This enum contains the available states of :attr:`telegram.SuggestedPostInfo.state`. + The enum members of this enumeration are instances + of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + PENDING = "pending" + """:obj:`str`: Suggested post is pending.""" + APPROVED = "approved" + """:obj:`str`: Suggested post was approved.""" + DECLINED = "declined" + """:obj:`str`: Suggested post was declined. + """ + + class ReactionType(StringEnum): """This enum contains the available types of :class:`telegram.ReactionType`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. diff --git a/src/telegram/ext/filters.py b/src/telegram/ext/filters.py index 3aa31a3900c..4d8908e8140 100644 --- a/src/telegram/ext/filters.py +++ b/src/telegram/ext/filters.py @@ -73,6 +73,7 @@ "SENDER_BOOST_COUNT", "STORY", "SUCCESSFUL_PAYMENT", + "SUGGESTED_POST_INFO", "TEXT", "USER", "USER_ATTACHMENT", @@ -1989,6 +1990,8 @@ def filter(self, update: Update) -> bool: or StatusUpdate.PINNED_MESSAGE.check_update(update) or StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update) or StatusUpdate.REFUNDED_PAYMENT.check_update(update) + or StatusUpdate.SUGGESTED_POST_APPROVAL_FAILED.check_update(update) + or StatusUpdate.SUGGESTED_POST_APPROVED.check_update(update) or StatusUpdate.SUGGESTED_POST_DECLINED.check_update(update) or StatusUpdate.SUGGESTED_POST_PAID.check_update(update) or StatusUpdate.SUGGESTED_POST_REFUNDED.check_update(update) @@ -2313,6 +2316,32 @@ def filter(self, message: Message) -> bool: .. versionadded:: 21.4 """ + class _SuggestedPostApprovalFailed(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.suggested_post_approval_failed) + + SUGGESTED_POST_APPROVAL_FAILED = _SuggestedPostApprovalFailed( + "filters.StatusUpdate.SUGGESTED_POST_APPROVAL_FAILED" + ) + """Messages that contain :attr:`telegram.Message.suggested_post_approval_failed`. + .. versionadded:: NEXT.VERSION + """ + + class _SuggestedPostApproved(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.suggested_post_approved) + + SUGGESTED_POST_APPROVED = _SuggestedPostApproved( + "filters.StatusUpdate.SUGGESTED_POST_APPROVED" + ) + """Messages that contain :attr:`telegram.Message.suggested_post_approved`. + .. versionadded:: NEXT.VERSION + """ + class _SuggestedPostDeclined(MessageFilter): __slots__ = () @@ -2596,6 +2625,20 @@ def filter(self, message: Message) -> bool: """Messages that contain :attr:`telegram.Message.successful_payment`.""" +class _SuggestedPostInfo(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.suggested_post_info) + + +SUGGESTED_POST_INFO = _SuggestedPostInfo(name="filters.SUGGESTED_POST_INFO") +"""Messages that contain :attr:`telegram.Message.suggested_post_info`. + +.. versionadded:: NEXT.VERSION +""" + + class Text(MessageFilter): """Text Messages. If a list of strings is passed, it filters messages to only allow those whose text is appearing in the given list. diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index a454abe042d..fdaa673f922 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -1101,6 +1101,16 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.REFUNDED_PAYMENT.check_update(update) update.message.refunded_payment = None + update.message.suggested_post_approval_failed = "suggested_post_approval_failed" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.SUGGESTED_POST_APPROVAL_FAILED.check_update(update) + update.message.suggested_post_approval_failed = None + + update.message.suggested_post_approved = "suggested_post_approved" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.SUGGESTED_POST_APPROVED.check_update(update) + update.message.suggested_post_approved = None + update.message.suggested_post_declined = "suggested_post_declined" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.SUGGESTED_POST_DECLINED.check_update(update) @@ -2128,6 +2138,11 @@ def test_filters_successful_payment(self, update): update.message.successful_payment = "test" assert filters.SUCCESSFUL_PAYMENT.check_update(update) + def test_filters_suggested_post_info(self, update): + assert not filters.SUGGESTED_POST_INFO.check_update(update) + update.message.suggested_post_info = "test" + assert filters.SUGGESTED_POST_INFO.check_update(update) + def test_filters_successful_payment_payloads(self, update): assert not filters.SuccessfulPayment(("custom-payload",)).check_update(update) assert not filters.SuccessfulPayment().check_update(update) diff --git a/tests/test_message.py b/tests/test_message.py index eed6fa5c64c..2ca208fd565 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -71,6 +71,13 @@ Sticker, Story, SuccessfulPayment, + SuggestedPostApprovalFailed, + SuggestedPostApproved, + SuggestedPostDeclined, + SuggestedPostInfo, + SuggestedPostPaid, + SuggestedPostPrice, + SuggestedPostRefunded, TextQuote, UniqueGift, UniqueGiftBackdrop, @@ -92,7 +99,6 @@ WebAppData, ) from telegram._directmessagestopic import DirectMessagesTopic -from telegram._suggestedpost import SuggestedPostDeclined, SuggestedPostPaid, SuggestedPostRefunded from telegram._utils.datetime import UTC from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ODVInput @@ -390,6 +396,30 @@ def message(bot): ), ) }, + { + "suggested_post_approved": SuggestedPostApproved( + send_date=dtm.datetime.utcnow(), + price=SuggestedPostPrice(currency="XTR", amount=100), + suggested_post_message=Message( + 7, dtm.datetime.utcnow(), Chat(13, "channel"), User(9, "i", False) + ), + ) + }, + { + "suggested_post_approval_failed": SuggestedPostApprovalFailed( + price=SuggestedPostPrice(currency="XTR", amount=100), + suggested_post_message=Message( + 7, dtm.datetime.utcnow(), Chat(13, "channel"), User(9, "i", False) + ), + ) + }, + { + "suggested_post_info": SuggestedPostInfo( + state="pending", + price=SuggestedPostPrice(currency="XTR", amount=100), + send_date=dtm.datetime.utcnow(), + ) + }, ], ids=[ "reply", @@ -477,6 +507,9 @@ def message(bot): "suggested_post_declined", "suggested_post_paid", "suggested_post_refunded", + "suggested_post_approved", + "suggested_post_approval_failed", + "suggested_post_info", ], ) def message_params(bot, request): diff --git a/tests/test_suggestedpost.py b/tests/test_suggestedpost.py index 625c5f85d12..dd6c08381aa 100644 --- a/tests/test_suggestedpost.py +++ b/tests/test_suggestedpost.py @@ -26,13 +26,17 @@ from telegram._message import Message from telegram._payment.stars.staramount import StarAmount from telegram._suggestedpost import ( + SuggestedPostApprovalFailed, + SuggestedPostApproved, SuggestedPostDeclined, + SuggestedPostInfo, SuggestedPostPaid, SuggestedPostParameters, SuggestedPostPrice, SuggestedPostRefunded, ) from telegram._utils.datetime import UTC, to_timestamp +from telegram.constants import SuggestedPostInfoState from tests.auxil.slots import mro_slots @@ -114,6 +118,90 @@ def test_equality(self, suggested_post_parameters): assert hash(a) != hash(e) +@pytest.fixture(scope="module") +def suggested_post_info(): + return SuggestedPostInfo( + state=SuggestedPostInfoTestBase.state, + price=SuggestedPostInfoTestBase.price, + send_date=SuggestedPostInfoTestBase.send_date, + ) + + +class SuggestedPostInfoTestBase: + state = SuggestedPostInfoState.PENDING + price = SuggestedPostPrice(currency="XTR", amount=100) + send_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) + + +class TestSuggestedPostInfoWithoutRequest(SuggestedPostInfoTestBase): + def test_slot_behaviour(self, suggested_post_info): + for attr in suggested_post_info.__slots__: + assert getattr(suggested_post_info, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(suggested_post_info)) == len(set(mro_slots(suggested_post_info))), ( + "duplicate slot" + ) + + def test_type_enum_conversion(self): + assert type(SuggestedPostInfo("pending").state) is SuggestedPostInfoState + assert SuggestedPostInfo("unknown").state == "unknown" + + def test_de_json(self, offline_bot): + json_dict = { + "state": self.state, + "price": self.price.to_dict(), + "send_date": to_timestamp(self.send_date), + } + spi = SuggestedPostInfo.de_json(json_dict, offline_bot) + assert spi.state == self.state + assert spi.price == self.price + assert spi.send_date == self.send_date + assert spi.api_kwargs == {} + + def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): + json_dict = { + "state": self.state, + "price": self.price.to_dict(), + "send_date": to_timestamp(self.send_date), + } + + spi_bot = SuggestedPostInfo.de_json(json_dict, offline_bot) + spi_bot_raw = SuggestedPostInfo.de_json(json_dict, raw_bot) + spi_bot_tz = SuggestedPostInfo.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing tzinfo objects is not reliable + send_date_offset = spi_bot_tz.send_date.utcoffset() + send_date_offset_tz = tz_bot.defaults.tzinfo.utcoffset( + spi_bot_tz.send_date.replace(tzinfo=None) + ) + + assert spi_bot.send_date.tzinfo == UTC + assert spi_bot_raw.send_date.tzinfo == UTC + assert send_date_offset_tz == send_date_offset + + def test_to_dict(self, suggested_post_info): + spi_dict = suggested_post_info.to_dict() + + assert isinstance(spi_dict, dict) + assert spi_dict["state"] == self.state + assert spi_dict["price"] == self.price.to_dict() + assert spi_dict["send_date"] == to_timestamp(self.send_date) + + def test_equality(self, suggested_post_info): + a = suggested_post_info + b = SuggestedPostInfo(state=self.state, price=self.price) + c = SuggestedPostInfo(state=SuggestedPostInfoState.DECLINED, price=self.price) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != e + assert hash(a) != hash(e) + + @pytest.fixture(scope="module") def suggested_post_price(): return SuggestedPostPrice( @@ -358,3 +446,155 @@ def test_equality(self, suggested_post_refunded): assert a != e assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def suggested_post_approved(): + return SuggestedPostApproved( + send_date=SuggestedPostApprovedTestBase.send_date, + suggested_post_message=SuggestedPostApprovedTestBase.suggested_post_message, + price=SuggestedPostApprovedTestBase.price, + ) + + +class SuggestedPostApprovedTestBase: + send_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) + suggested_post_message = Message(1, dtm.datetime.now(), Chat(1, ""), text="post this pls.") + price = SuggestedPostPrice(currency="XTR", amount=100) + + +class TestSuggestedPostApprovedWithoutRequest(SuggestedPostApprovedTestBase): + def test_slot_behaviour(self, suggested_post_approved): + for attr in suggested_post_approved.__slots__: + assert getattr(suggested_post_approved, attr, "err") != "err", ( + f"got extra slot '{attr}'" + ) + assert len(mro_slots(suggested_post_approved)) == len( + set(mro_slots(suggested_post_approved)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "send_date": to_timestamp(self.send_date), + "suggested_post_message": self.suggested_post_message.to_dict(), + "price": self.price.to_dict(), + } + spa = SuggestedPostApproved.de_json(json_dict, offline_bot) + assert spa.send_date == self.send_date + assert spa.suggested_post_message == self.suggested_post_message + assert spa.price == self.price + assert spa.api_kwargs == {} + + def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): + json_dict = { + "send_date": to_timestamp(self.send_date), + "suggested_post_message": self.suggested_post_message.to_dict(), + "price": self.price.to_dict(), + } + + spa_bot = SuggestedPostApproved.de_json(json_dict, offline_bot) + spa_bot_raw = SuggestedPostApproved.de_json(json_dict, raw_bot) + spi_bot_tz = SuggestedPostApproved.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing tzinfo objects is not reliable + send_date_offset = spi_bot_tz.send_date.utcoffset() + send_date_offset_tz = tz_bot.defaults.tzinfo.utcoffset( + spi_bot_tz.send_date.replace(tzinfo=None) + ) + + assert spa_bot.send_date.tzinfo == UTC + assert spa_bot_raw.send_date.tzinfo == UTC + assert send_date_offset_tz == send_date_offset + + def test_to_dict(self, suggested_post_approved): + spa_dict = suggested_post_approved.to_dict() + + assert isinstance(spa_dict, dict) + assert spa_dict["send_date"] == to_timestamp(self.send_date) + assert spa_dict["suggested_post_message"] == self.suggested_post_message.to_dict() + assert spa_dict["price"] == self.price.to_dict() + + def test_equality(self, suggested_post_approved): + a = suggested_post_approved + b = SuggestedPostApproved( + send_date=self.send_date, + suggested_post_message=self.suggested_post_message, + price=self.price, + ) + c = SuggestedPostApproved( + send_date=self.send_date, + suggested_post_message=self.suggested_post_message, + price=SuggestedPostPrice(currency="XTR", amount=self.price.amount - 1), + ) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def suggested_post_approval_failed(): + return SuggestedPostApprovalFailed( + price=SuggestedPostApprovalFailedTestBase.price, + suggested_post_message=SuggestedPostApprovalFailedTestBase.suggested_post_message, + ) + + +class SuggestedPostApprovalFailedTestBase: + price = SuggestedPostPrice(currency="XTR", amount=100) + suggested_post_message = Message(1, dtm.datetime.now(), Chat(1, ""), text="post this pls.") + + +class TestSuggestedPostApprovalFailedWithoutRequest(SuggestedPostApprovalFailedTestBase): + def test_slot_behaviour(self, suggested_post_approval_failed): + for attr in suggested_post_approval_failed.__slots__: + assert getattr(suggested_post_approval_failed, attr, "err") != "err", ( + f"got extra slot '{attr}'" + ) + assert len(mro_slots(suggested_post_approval_failed)) == len( + set(mro_slots(suggested_post_approval_failed)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "price": self.price.to_dict(), + "suggested_post_message": self.suggested_post_message.to_dict(), + } + spaf = SuggestedPostApprovalFailed.de_json(json_dict, offline_bot) + assert spaf.price == self.price + assert spaf.suggested_post_message == self.suggested_post_message + assert spaf.api_kwargs == {} + + def test_to_dict(self, suggested_post_approval_failed): + spaf_dict = suggested_post_approval_failed.to_dict() + + assert isinstance(spaf_dict, dict) + assert spaf_dict["price"] == self.price.to_dict() + assert spaf_dict["suggested_post_message"] == self.suggested_post_message.to_dict() + + def test_equality(self, suggested_post_approval_failed): + a = suggested_post_approval_failed + b = SuggestedPostApprovalFailed( + price=self.price, + suggested_post_message=self.suggested_post_message, + ) + c = SuggestedPostApprovalFailed( + price=SuggestedPostPrice(currency="XTR", amount=self.price.amount - 1), + suggested_post_message=self.suggested_post_message, + ) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != e + assert hash(a) != hash(e) From cfc6ad04e53adadecd4c254fde815f748fed54c0 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 7 Sep 2025 22:23:41 +0200 Subject: [PATCH 14/17] Add Parameter `can_manage_direct_messages` to `promote_chat_member` (#4935) --- changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml | 1 + src/telegram/_bot.py | 7 +++++++ src/telegram/_chat.py | 2 ++ src/telegram/ext/_extbot.py | 2 ++ tests/test_bot.py | 3 +++ 5 files changed, 15 insertions(+) diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index 105a27270f5..97e06ec15ba 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -8,5 +8,6 @@ pull_requests = [ { uid = "4916", author_uid = "harshil21"}, { uid = "4912", author_uid = "aelkheir" }, { uid = "4921", author_uid = "aelkheir" }, + { uid = "4935", author_uid = "Bibo-Joshi" }, { uid = "4931", author_uid = "aelkheir" }, ] diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 54bdfcb701c..f4fc7881ab4 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -5850,6 +5850,7 @@ async def promote_chat_member( can_post_stories: Optional[bool] = None, can_edit_stories: Optional[bool] = None, can_delete_stories: Optional[bool] = None, + can_manage_direct_messages: Optional[bool] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -5918,6 +5919,11 @@ async def promote_chat_member( delete stories posted by other users. .. versionadded:: 20.6 + can_manage_direct_messages (:obj:`bool`, optional): Pass :obj:`True`, if the + administrator can manage direct messages within the channel and decline suggested + posts; for channels only + + .. versionadded:: NEXT.VERSION Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -5944,6 +5950,7 @@ async def promote_chat_member( "can_post_stories": can_post_stories, "can_edit_stories": can_edit_stories, "can_delete_stories": can_delete_stories, + "can_manage_direct_messages": can_manage_direct_messages, } return await self._post( diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index 77eb2d9a386..a39b0809763 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -616,6 +616,7 @@ async def promote_member( can_post_stories: Optional[bool] = None, can_edit_stories: Optional[bool] = None, can_delete_stories: Optional[bool] = None, + can_manage_direct_messages: Optional[bool] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -666,6 +667,7 @@ async def promote_member( can_post_stories=can_post_stories, can_edit_stories=can_edit_stories, can_delete_stories=can_delete_stories, + can_manage_direct_messages=can_manage_direct_messages, ) async def restrict_member( diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index 65b6e1f2fa4..7670099a691 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -2357,6 +2357,7 @@ async def promote_chat_member( can_post_stories: Optional[bool] = None, can_edit_stories: Optional[bool] = None, can_delete_stories: Optional[bool] = None, + can_manage_direct_messages: Optional[bool] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2383,6 +2384,7 @@ async def promote_chat_member( can_post_stories=can_post_stories, can_edit_stories=can_edit_stories, can_delete_stories=can_delete_stories, + can_manage_direct_messages=can_manage_direct_messages, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, diff --git a/tests/test_bot.py b/tests/test_bot.py index 66c77316abb..e2c905c1adb 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -3625,6 +3625,7 @@ async def test_promote_chat_member(self, bot, channel_id, monkeypatch): can_post_stories=True, can_edit_stories=True, can_delete_stories=True, + can_manage_direct_messages=True, ) # Test that we pass the correct params to TG @@ -3648,6 +3649,7 @@ async def make_assertion(*args, **_): and data.get("can_post_stories") == 13 and data.get("can_edit_stories") == 14 and data.get("can_delete_stories") == 15 + and data.get("can_manage_direct_messages") == 16 ) monkeypatch.setattr(bot, "_post", make_assertion) @@ -3669,6 +3671,7 @@ async def make_assertion(*args, **_): can_post_stories=13, can_edit_stories=14, can_delete_stories=15, + can_manage_direct_messages=16, ) async def test_export_chat_invite_link(self, bot, channel_id): From b3185941ac48b92568de7131b534ca7143c3d355 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 8 Sep 2025 06:38:00 +0200 Subject: [PATCH 15/17] Add Parameter `message_thread_id` to `send_paid_media` (#4936) --- changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml | 1 + src/telegram/_bot.py | 5 +++++ src/telegram/_chat.py | 2 ++ src/telegram/_message.py | 4 ++++ src/telegram/ext/_extbot.py | 2 ++ tests/test_message.py | 1 + 6 files changed, 15 insertions(+) diff --git a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml index 97e06ec15ba..9b7a7cc4080 100644 --- a/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml +++ b/changes/unreleased/4911.kiF45Y4cfPGMq5cuLpa5da.toml @@ -8,6 +8,7 @@ pull_requests = [ { uid = "4916", author_uid = "harshil21"}, { uid = "4912", author_uid = "aelkheir" }, { uid = "4921", author_uid = "aelkheir" }, + { uid = "4936", author_uid = "Bibo-Joshi" }, { uid = "4935", author_uid = "Bibo-Joshi" }, { uid = "4931", author_uid = "aelkheir" }, ] diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index f4fc7881ab4..55697aacac4 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -10945,6 +10945,7 @@ async def send_paid_media( allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, suggested_post_parameters: Optional["SuggestedPostParameters"] = None, + message_thread_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -10998,6 +10999,9 @@ async def send_paid_media( direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| .. versionadded:: NEXT.VERSION + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -11044,6 +11048,7 @@ async def send_paid_media( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_thread_id=message_thread_id, ) async def create_chat_subscription_invite_link( diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index a39b0809763..f1c3cf7aeae 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -3554,6 +3554,7 @@ async def send_paid_media( allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, suggested_post_parameters: Optional["SuggestedPostParameters"] = None, + message_thread_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3599,6 +3600,7 @@ async def send_paid_media( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_thread_id=message_thread_id, ) async def send_gift( diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 5c81a05367a..10b5ccb8353 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -4038,6 +4038,7 @@ async def reply_paid_media( payload: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, suggested_post_parameters: Optional["SuggestedPostParameters"] = None, + message_thread_id: ODVInput[int] = DEFAULT_NONE, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -4052,6 +4053,7 @@ async def reply_paid_media( await bot.send_paid_media( chat_id=message.chat.id, + message_thread_id=update.effective_message.message_thread_id, business_connection_id=message.business_connection_id, direct_messages_topic_id=self.direct_messages_topic.topic_id, *args, @@ -4072,6 +4074,7 @@ async def reply_paid_media( chat_id, effective_reply_parameters = await self._parse_quote_arguments( do_quote, reply_to_message_id, reply_parameters, allow_sending_without_reply ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_paid_media( chat_id=chat_id, caption=caption, @@ -4094,6 +4097,7 @@ async def reply_paid_media( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), suggested_post_parameters=suggested_post_parameters, + message_thread_id=message_thread_id, ) async def edit_text( diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index 7670099a691..78b24727d1c 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -4986,6 +4986,7 @@ async def send_paid_media( allow_paid_broadcast: Optional[bool] = None, direct_messages_topic_id: Optional[int] = None, suggested_post_parameters: Optional["SuggestedPostParameters"] = None, + message_thread_id: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -5020,6 +5021,7 @@ async def send_paid_media( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_thread_id=message_thread_id, ) async def create_chat_subscription_invite_link( diff --git a/tests/test_message.py b/tests/test_message.py index 2ca208fd565..81fa66f792d 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -2718,6 +2718,7 @@ async def make_assertion(*_, **kwargs): "direct_messages_topic_id", ], ["do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( message.reply_paid_media, From 2ac87a84fa75bc86e50cb20eee7ff6a0daaa4016 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Mon, 8 Sep 2025 07:49:49 +0300 Subject: [PATCH 16/17] Use regular `str` annotation for `currency` and `state` --- src/telegram/_suggestedpost.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/telegram/_suggestedpost.py b/src/telegram/_suggestedpost.py index a6be1407ccb..f86090927a7 100644 --- a/src/telegram/_suggestedpost.py +++ b/src/telegram/_suggestedpost.py @@ -19,7 +19,7 @@ """This module contains objects related to Telegram suggested posts.""" import datetime as dtm -from typing import TYPE_CHECKING, Final, Literal, Optional +from typing import TYPE_CHECKING, Final, Optional from telegram import constants from telegram._message import Message @@ -74,13 +74,13 @@ class SuggestedPostPrice(TelegramObject): def __init__( self, - currency: Literal["XTR", "TON"], + currency: str, amount: int, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) - self.currency: Literal["XTR", "TON"] = currency + self.currency: str = currency self.amount: int = amount self._id_attrs = (self.currency, self.amount) @@ -206,7 +206,7 @@ class SuggestedPostInfo(TelegramObject): def __init__( self, - state: Literal["pending", "approved", "declined"], + state: str, price: Optional[SuggestedPostPrice] = None, send_date: Optional[dtm.datetime] = None, *, @@ -339,7 +339,7 @@ class SuggestedPostPaid(TelegramObject): def __init__( self, - currency: Literal["XTR", "TON"], + currency: str, suggested_post_message: Optional[Message] = None, amount: Optional[int] = None, star_amount: Optional[StarAmount] = None, @@ -348,7 +348,7 @@ def __init__( ): super().__init__(api_kwargs=api_kwargs) # Required - self.currency: Literal["XTR", "TON"] = currency + self.currency: str = currency # Optionals self.suggested_post_message: Optional[Message] = suggested_post_message self.amount: Optional[int] = amount From 5b15b3ce7558ea5e2282de180e07afe62ba1135b Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:00:36 +0300 Subject: [PATCH 17/17] Fix `test_official` check of `DirectMessagesTopic.user` hope i didn't interupt, Hinrich :) --- src/telegram/_directmessagestopic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telegram/_directmessagestopic.py b/src/telegram/_directmessagestopic.py index c4eaed4f16a..304e496ec98 100644 --- a/src/telegram/_directmessagestopic.py +++ b/src/telegram/_directmessagestopic.py @@ -63,7 +63,7 @@ class DirectMessagesTopic(TelegramObject): __slots__ = ("topic_id", "user") def __init__( - self, topic_id: int, user: Optional[User], *, api_kwargs: Optional[JSONDict] = None + self, topic_id: int, user: Optional[User] = None, *, api_kwargs: Optional[JSONDict] = None ): super().__init__(api_kwargs=api_kwargs)