From 07880e287602d8e2c49bcb1c481ab0082634f94b Mon Sep 17 00:00:00 2001 From: poolitzer Date: Wed, 12 Feb 2025 14:49:54 +0100 Subject: [PATCH 01/20] WIP --- examples/echobot.py | 23 ++++++- telegram/_bot.py | 28 ++++++-- telegram/_payment/stars/transactionpartner.py | 64 +++++++++++++++++++ telegram/constants.py | 5 ++ 4 files changed, 111 insertions(+), 9 deletions(-) diff --git a/examples/echobot.py b/examples/echobot.py index b2ccdc139f2..8b6dd55f9fa 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -16,8 +16,9 @@ """ import logging +import asyncio -from telegram import ForceReply, Update +from telegram import ForceReply, Update, error from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters # Enable logging @@ -54,7 +55,7 @@ async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: def main() -> None: """Start the bot.""" # Create the Application and pass it your bot's token. - application = Application.builder().token("TOKEN").build() + application = Application.builder().token("6134338481:AAHxO-DxfxjCu7HL67VMFDWWrv6E8rZexRQ").build() # on different commands - answer in Telegram application.add_handler(CommandHandler("start", start)) @@ -64,7 +65,23 @@ def main() -> None: application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) # Run the bot until the user presses Ctrl-C - application.run_polling(allowed_updates=Update.ALL_TYPES) + loop = asyncio.get_event_loop() + while True: + try: + loop.run_until_complete(application.initialize()) + except error.NetworkError: + loop.run_until_complete(asyncio.sleep(2)) + pass + else: + break + loop.run_until_complete(application.start()) + loop.run_until_complete(application.updater.start_polling()) + try: + loop.run_forever() + except (KeyboardInterrupt, SystemExit): + loop.run_until_complete(application.updater.stop()) + loop.run_until_complete(application.stop()) + loop.run_until_complete(application.shutdown()) if __name__ == "__main__": diff --git a/telegram/_bot.py b/telegram/_bot.py index f1d4be176f0..353f393cca2 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9804,7 +9804,7 @@ async def get_available_gifts( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, ) -> Gifts: - """Returns the list of gifts that can be sent by the bot to users. + """Returns the list of gifts that can be sent by the bot to users and channel chats. Requires no parameters. .. versionadded:: 21.8 @@ -9828,12 +9828,13 @@ async def get_available_gifts( async def send_gift( self, - user_id: int, - gift_id: Union[str, Gift], + user_id: Optional[int] = None, + gift_id: Union[str, Gift] = None, text: Optional[str] = None, text_parse_mode: ODVInput[str] = DEFAULT_NONE, text_entities: Optional[Sequence["MessageEntity"]] = None, pay_for_upgrade: Optional[bool] = None, + chat_id: Optional[Union[str, int]] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -9841,15 +9842,23 @@ async def send_gift( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, ) -> bool: - """Sends a gift to the given user. - The gift can't be converted to Telegram Stars by the user + """Sends a gift to the given user or channel chat. + The gift can't be converted to Telegram Stars by the receiver. .. versionadded:: 21.8 Args: - user_id (:obj:`int`): Unique identifier of the target user that will receive the gift + user_id (:obj:`int`, optional): Required if :paramref:`chat_id` is not specified. + Unique identifier of the target user that will receive the gift. + + .. versionchanged:: NEXT.VERSION + Now optional. gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a :class:`~telegram.Gift` object + chat_id (:obj:`int` | :obj:`str`, optional): Required if :paramref:`user_id` + is not specified. |chat_id_channel| It will receive the gift. + + .. versionadded:: NEXT.VERSION text (:obj:`str`, optional): Text that will be shown along with the gift; 0- :tg-const:`telegram.constants.GiftLimit.MAX_TEXT_LENGTH` characters text_parse_mode (:obj:`str`, optional): Mode for parsing entities. @@ -9876,6 +9885,12 @@ async def send_gift( Raises: :class:`telegram.error.TelegramError` """ + # TODO: Remove when stability policy allows, tags: deprecated NEXT.VERSION + # also we should raise a deprecation warnung if anything is passed by + # position since it will be moved, not sure how + if gift_id is None: + raise TypeError("This was temporarily moved to optional," + " you must pass it as the description states.") data: JSONDict = { "user_id": user_id, "gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id, @@ -9883,6 +9898,7 @@ async def send_gift( "text_parse_mode": text_parse_mode, "text_entities": text_entities, "pay_for_upgrade": pay_for_upgrade, + "chat_id": chat_id, } return await self._post( "sendGift", diff --git a/telegram/_payment/stars/transactionpartner.py b/telegram/_payment/stars/transactionpartner.py index 860fccff17d..55e598ebe7b 100644 --- a/telegram/_payment/stars/transactionpartner.py +++ b/telegram/_payment/stars/transactionpartner.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Final, Optional from telegram import constants +from telegram._chat import Chat from telegram._gifts import Gift from telegram._paidmedia import PaidMedia from telegram._telegramobject import TelegramObject @@ -43,6 +44,7 @@ class TransactionPartner(TelegramObject): transactions. Currently, it can be one of: * :class:`TransactionPartnerUser` + * :class:`TransactionPartnerChat` * :class:`TransactionPartnerAffiliateProgram` * :class:`TransactionPartnerFragment` * :class:`TransactionPartnerTelegramAds` @@ -54,6 +56,9 @@ class TransactionPartner(TelegramObject): .. versionadded:: 21.4 + ..versionchanged:: NEXT.VERSION + Added :class:`TransactionPartnerChat` + Args: type (:obj:`str`): The type of the transaction partner. @@ -68,6 +73,11 @@ class TransactionPartner(TelegramObject): .. versionadded:: 21.9 """ + CHAT: Final[str] = constants.TransactionPartnerType.CHAT + """:const:`telegram.constants.TransactionPartnerType.CHAT` + + .. versionadded:: NEXT.VERSION + """ FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT """:const:`telegram.constants.TransactionPartnerType.FRAGMENT`""" OTHER: Final[str] = constants.TransactionPartnerType.OTHER @@ -103,6 +113,7 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPar _class_mapping: dict[str, type[TransactionPartner]] = { cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram, + cls.CHAT: TransactionPartnerChat, cls.FRAGMENT: TransactionPartnerFragment, cls.USER: TransactionPartnerUser, cls.TELEGRAM_ADS: TransactionPartnerTelegramAds, @@ -171,6 +182,59 @@ def de_json( return super().de_json(data=data, bot=bot) # type: ignore[return-value] +class TransactionPartnerChat(TransactionPartner): + """Describes a transaction with a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + chat (:class:`telegram.Chat`): Information about the chat. + gift (:class:`telegram.Gift`, optional): The gift sent to the chat by the bot. + + Attributes: + type (:obj:`str`): The type of the transaction partner, + always :tg-const:`telegram.TransactionPartner.CHAT`. + chat (:class:`telegram.Chat`): Information about the chat. + gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot. + + """ + + __slots__ = ( + "chat", + "gift", + ) + + def __init__( + self, + chat: Chat, + gift: Optional[Gift] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=TransactionPartner.CHAT, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.chat: Chat = chat + self.gift: Optional[Gift] = gift + + self._id_attrs = ( + self.type, + self.chat, + ) + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPartnerChat": + """See :meth:`telegram.TransactionPartner.de_json`.""" + data = cls._parse_data(data) + + data["chat"] = de_json_optional(data.get("chat"), Chat, bot) + data["gift"] = de_json_optional(data.get("gift"), Gift, bot) + + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + class TransactionPartnerFragment(TransactionPartner): """Describes a withdrawal transaction with Fragment. diff --git a/telegram/constants.py b/telegram/constants.py index c61b3b96aab..8a93bfe0480 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -2659,6 +2659,11 @@ class TransactionPartnerType(StringEnum): .. versionadded:: 21.9 """ + CHAT = "chat" + """:obj:`str`: Transaction with a chat. + + .. versionadded:: NEXT.VERSION + """ FRAGMENT = "fragment" """:obj:`str`: Withdrawal transaction with Fragment.""" OTHER = "other" From edd6c840ee3542927a2d6bb82971b264d60a85f0 Mon Sep 17 00:00:00 2001 From: poolitzer Date: Wed, 12 Feb 2025 16:10:42 +0100 Subject: [PATCH 02/20] revert this change wuppsi --- examples/echobot.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/examples/echobot.py b/examples/echobot.py index 8b6dd55f9fa..b2ccdc139f2 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -16,9 +16,8 @@ """ import logging -import asyncio -from telegram import ForceReply, Update, error +from telegram import ForceReply, Update from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters # Enable logging @@ -55,7 +54,7 @@ async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: def main() -> None: """Start the bot.""" # Create the Application and pass it your bot's token. - application = Application.builder().token("6134338481:AAHxO-DxfxjCu7HL67VMFDWWrv6E8rZexRQ").build() + application = Application.builder().token("TOKEN").build() # on different commands - answer in Telegram application.add_handler(CommandHandler("start", start)) @@ -65,23 +64,7 @@ def main() -> None: application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) # Run the bot until the user presses Ctrl-C - loop = asyncio.get_event_loop() - while True: - try: - loop.run_until_complete(application.initialize()) - except error.NetworkError: - loop.run_until_complete(asyncio.sleep(2)) - pass - else: - break - loop.run_until_complete(application.start()) - loop.run_until_complete(application.updater.start_polling()) - try: - loop.run_forever() - except (KeyboardInterrupt, SystemExit): - loop.run_until_complete(application.updater.stop()) - loop.run_until_complete(application.stop()) - loop.run_until_complete(application.shutdown()) + application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": From 3f9b0fb3df4f10ee46b26724f69b13302643edad Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Thu, 13 Feb 2025 11:11:11 +0100 Subject: [PATCH 03/20] Fix: Better error wording Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- telegram/_bot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 353f393cca2..3cdd1a50d15 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9889,8 +9889,7 @@ async def send_gift( # also we should raise a deprecation warnung if anything is passed by # position since it will be moved, not sure how if gift_id is None: - raise TypeError("This was temporarily moved to optional," - " you must pass it as the description states.") + raise TypeError("Missing required argument `gift_id`.") data: JSONDict = { "user_id": user_id, "gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id, From 6b8b8fb78d9233d7f369931c73027a4b38997bed Mon Sep 17 00:00:00 2001 From: poolitzer Date: Fri, 14 Feb 2025 18:59:09 +0100 Subject: [PATCH 04/20] More work --- docs/source/telegram.transactionpartnerchat.rst | 7 +++++++ telegram/_chatfullinfo.py | 9 +++++++++ 2 files changed, 16 insertions(+) create mode 100644 docs/source/telegram.transactionpartnerchat.rst diff --git a/docs/source/telegram.transactionpartnerchat.rst b/docs/source/telegram.transactionpartnerchat.rst new file mode 100644 index 00000000000..def37495344 --- /dev/null +++ b/docs/source/telegram.transactionpartnerchat.rst @@ -0,0 +1,7 @@ +TransactionPartnerUser +====================== + +.. autoclass:: telegram.TransactionPartnerUser + :members: + :show-inheritance: + :inherited-members: TransactionPartner diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index c763efca78e..f7266f4d39c 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -200,6 +200,9 @@ class ChatFullInfo(_ChatBase): sent or forwarded to the channel chat. The field is available only for channel chats. .. versionadded:: 21.4 + can_send_gift (:obj:`bool`, optional): :obj:`True`, if gifts can be sent to the chat. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this chat. @@ -354,6 +357,9 @@ class ChatFullInfo(_ChatBase): sent or forwarded to the channel chat. The field is available only for channel chats. .. versionadded:: 21.4 + can_send_gift (:obj:`bool`): Optional. :obj:`True`, if gifts can be sent to the chat. + + .. 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 @@ -369,6 +375,7 @@ class ChatFullInfo(_ChatBase): "business_intro", "business_location", "business_opening_hours", + "can_send_gift", "can_send_paid_media", "can_set_sticker_set", "custom_emoji_sticker_set_name", @@ -445,6 +452,7 @@ def __init__( linked_chat_id: Optional[int] = None, location: Optional[ChatLocation] = None, can_send_paid_media: Optional[bool] = None, + can_send_gift: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -510,6 +518,7 @@ def __init__( self.business_location: Optional[BusinessLocation] = business_location self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours self.can_send_paid_media: Optional[bool] = can_send_paid_media + self.can_send_gift: Optional[bool] = can_send_gift @classmethod def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo": From 3e64b1c6896711f87cc0d2c506903ea45ec04a12 Mon Sep 17 00:00:00 2001 From: poolitzer Date: Fri, 14 Feb 2025 19:00:22 +0100 Subject: [PATCH 05/20] more stuff --- docs/source/telegram.payments-tree.rst | 1 + docs/source/telegram.transactionpartnerchat.rst | 4 ++-- telegram/__init__.py | 2 ++ telegram/_chat.py | 13 +++++++++++-- telegram/_user.py | 3 ++- telegram/ext/_extbot.py | 5 +++-- tests/auxil/bot_method_checks.py | 4 ++++ tests/test_chat.py | 4 +++- 8 files changed, 28 insertions(+), 8 deletions(-) diff --git a/docs/source/telegram.payments-tree.rst b/docs/source/telegram.payments-tree.rst index e8ec7bd3e3b..3e6f42bdc97 100644 --- a/docs/source/telegram.payments-tree.rst +++ b/docs/source/telegram.payments-tree.rst @@ -27,6 +27,7 @@ Your bot can accept payments from Telegram users. Please see the `introduction t telegram.successfulpayment telegram.transactionpartner telegram.transactionpartneraffiliateprogram + telegram.transactionpartnerchat telegram.transactionpartnerfragment telegram.transactionpartnerother telegram.transactionpartnertelegramads diff --git a/docs/source/telegram.transactionpartnerchat.rst b/docs/source/telegram.transactionpartnerchat.rst index def37495344..d57cf128378 100644 --- a/docs/source/telegram.transactionpartnerchat.rst +++ b/docs/source/telegram.transactionpartnerchat.rst @@ -1,7 +1,7 @@ -TransactionPartnerUser +TransactionPartnerChat ====================== -.. autoclass:: telegram.TransactionPartnerUser +.. autoclass:: telegram.TransactionPartnerChat :members: :show-inheritance: :inherited-members: TransactionPartner diff --git a/telegram/__init__.py b/telegram/__init__.py index a895e60dd1f..fe2fce247ea 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -238,6 +238,7 @@ "TextQuote", "TransactionPartner", "TransactionPartnerAffiliateProgram", + "TransactionPartnerChat", "TransactionPartnerFragment", "TransactionPartnerOther", "TransactionPartnerTelegramAds", @@ -275,6 +276,7 @@ from telegram._payment.stars.transactionpartner import ( TransactionPartner, TransactionPartnerAffiliateProgram, + TransactionPartnerChat, TransactionPartnerFragment, TransactionPartnerOther, TransactionPartnerTelegramAds, diff --git a/telegram/_chat.py b/telegram/_chat.py index ee4c25f59b6..6fd1d59427d 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -3462,18 +3462,27 @@ async def send_gift( await bot.send_gift(user_id=update.effective_chat.id, *args, **kwargs ) + or:: + + await bot.send_gift(chat_id=update.effective_chat.id, *args, **kwargs ) + For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`. Caution: - Can only work, if the chat is a private chat, see :attr:`type`. + Will only work if the chat is a private or channel chat, see :attr:`type`. .. versionadded:: 21.8 + .. versionchanged:: NEXT.VERSION + + Added support for channel chats. + Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return await self.get_bot().send_gift( - user_id=self.id, + chat_id=self.id if self.type == Chat.CHANNEL else None, + user_id=self.id if self.type == Chat.PRIVATE else None, gift_id=gift_id, text=text, text_parse_mode=text_parse_mode, diff --git a/telegram/_user.py b/telegram/_user.py index 0ef1d8d66d4..d28892e4135 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -1670,7 +1670,7 @@ async def send_gift( ) -> bool: """Shortcut for:: - await bot.send_gift( user_id=update.effective_user.id, *args, **kwargs ) + await bot.send_gift(user_id=update.effective_user.id, *args, **kwargs ) For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`. @@ -1680,6 +1680,7 @@ async def send_gift( :obj:`bool`: On success, :obj:`True` is returned. """ return await self.get_bot().send_gift( + chat_id=None, user_id=self.id, gift_id=gift_id, text=text, diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index e8f207e24ef..e00096e2388 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -4468,12 +4468,13 @@ async def get_available_gifts( async def send_gift( self, - user_id: int, - gift_id: Union[str, Gift], + user_id: Optional[int] = None, + gift_id: Union[str, Gift] = None, text: Optional[str] = None, text_parse_mode: ODVInput[str] = DEFAULT_NONE, text_entities: Optional[Sequence["MessageEntity"]] = None, pay_for_upgrade: Optional[bool] = None, + chat_id: Optional[Union[str, int]] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 8e3179ea944..657647d18d5 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -351,6 +351,9 @@ def build_kwargs( allow_sending_without_reply=manually_passed_value, quote_parse_mode=manually_passed_value, ) + # TODO remove when gift_id isnt marked as optional anymore, tags: deprecated NEXT.VERSION + elif name == "gift_id": + kws[name] = "GIFT-ID" return kws @@ -650,6 +653,7 @@ async def check_defaults_handling( expected_defaults_value=expected_defaults_value, ) request.post = assertion_callback + print(kwargs) await method(**kwargs) # 2: test that we get the manually passed non-None value diff --git a/tests/test_chat.py b/tests/test_chat.py index 27c3f934008..dd0c8e7f45d 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1321,7 +1321,9 @@ async def make_assertion(*_, **kwargs): and kwargs["text_entities"] == "text_entities" ) - assert check_shortcut_signature(Chat.send_gift, Bot.send_gift, ["user_id"], []) + # TODO discuss if better way exists + with pytest.raises(Exception, match="Default for argument gift_id does not match the default of the Bot method."): + assert check_shortcut_signature(Chat.send_gift, Bot.send_gift, ["user_id", "chat_id"], []) assert await check_shortcut_call(chat.send_gift, chat.get_bot(), "send_gift") assert await check_defaults_handling(chat.send_gift, chat.get_bot()) From cdfeb7c3b835d49db961ad6f769faa04dd81481e Mon Sep 17 00:00:00 2001 From: poolitzer Date: Sun, 16 Feb 2025 10:46:37 +0100 Subject: [PATCH 06/20] update tests --- tests/test_chat.py | 35 ++++++++++++++++++++++++++----- tests/test_official/exceptions.py | 2 ++ tests/test_user.py | 12 +++++++++-- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/tests/test_chat.py b/tests/test_chat.py index dd0c8e7f45d..39c427fe275 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1312,7 +1312,7 @@ async def make_assertion(*_, **kwargs): ) async def test_instance_method_send_gift(self, monkeypatch, chat): - async def make_assertion(*_, **kwargs): + async def make_assertion_private(*_, **kwargs): return ( kwargs["user_id"] == chat.id and kwargs["gift_id"] == "gift_id" @@ -1321,13 +1321,38 @@ async def make_assertion(*_, **kwargs): and kwargs["text_entities"] == "text_entities" ) + async def make_assertion_channel(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["gift_id"] == "gift_id" + and kwargs["text"] == "text" + and kwargs["text_parse_mode"] == "text_parse_mode" + and kwargs["text_entities"] == "text_entities" + ) + # TODO discuss if better way exists - with pytest.raises(Exception, match="Default for argument gift_id does not match the default of the Bot method."): - assert check_shortcut_signature(Chat.send_gift, Bot.send_gift, ["user_id", "chat_id"], []) - assert await check_shortcut_call(chat.send_gift, chat.get_bot(), "send_gift") + with pytest.raises( + Exception, + match="Default for argument gift_id does not match the default of the Bot method.", + ): + assert check_shortcut_signature( + Chat.send_gift, Bot.send_gift, ["user_id", "chat_id"], [] + ) + assert await check_shortcut_call( + chat.send_gift, chat.get_bot(), "send_gift", ["user_id", "chat_id"] + ) assert await check_defaults_handling(chat.send_gift, chat.get_bot()) - monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion) + monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion_private) + chat.type = chat.PRIVATE + assert await chat.send_gift( + gift_id="gift_id", + text="text", + text_parse_mode="text_parse_mode", + text_entities="text_entities", + ) + monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion_channel) + chat.type = chat.CHANNEL assert await chat.send_gift( gift_id="gift_id", text="text", diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 8bc2c84500a..f4fd5de33c4 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -198,6 +198,8 @@ def ptb_ignored_params(object_name: str) -> set[str]: "send_venue": {"latitude", "longitude", "title", "address"}, "send_contact": {"phone_number", "first_name"}, # ----> + # here for backwards compatibility + "send_gift": {"gift_id"}, } diff --git a/tests/test_user.py b/tests/test_user.py index 54657c59047..ae16798c42e 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -731,8 +731,16 @@ async def make_assertion(*_, **kwargs): and kwargs["text_entities"] == "text_entities" ) - assert check_shortcut_signature(user.send_gift, Bot.send_gift, ["user_id"], []) - assert await check_shortcut_call(user.send_gift, user.get_bot(), "send_gift") + with pytest.raises( + Exception, + match="Default for argument gift_id does not match the default of the Bot method.", + ): + assert check_shortcut_signature( + user.send_gift, Bot.send_gift, ["user_id", "chat_id"], [] + ) + assert await check_shortcut_call( + user.send_gift, user.get_bot(), "send_gift", ["chat_id", "user_id"] + ) assert await check_defaults_handling(user.send_gift, user.get_bot()) monkeypatch.setattr(user.get_bot(), "send_gift", make_assertion) From 14afad5b565071ea387c8ec6c1227ad4f72f77b2 Mon Sep 17 00:00:00 2001 From: poolitzer Date: Sun, 16 Feb 2025 11:18:25 +0100 Subject: [PATCH 07/20] make pre-commit happy --- telegram/_bot.py | 2 +- telegram/_payment/stars/transactionpartner.py | 1 + telegram/ext/_extbot.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 3cdd1a50d15..f8b53211ed8 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9829,7 +9829,7 @@ async def get_available_gifts( async def send_gift( self, user_id: Optional[int] = None, - gift_id: Union[str, Gift] = None, + gift_id: Union[str, Gift] = None, # type: ignore text: Optional[str] = None, text_parse_mode: ODVInput[str] = DEFAULT_NONE, text_entities: Optional[Sequence["MessageEntity"]] = None, diff --git a/telegram/_payment/stars/transactionpartner.py b/telegram/_payment/stars/transactionpartner.py index 55e598ebe7b..1bd56b00fe9 100644 --- a/telegram/_payment/stars/transactionpartner.py +++ b/telegram/_payment/stars/transactionpartner.py @@ -235,6 +235,7 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPar return super().de_json(data=data, bot=bot) # type: ignore[return-value] + class TransactionPartnerFragment(TransactionPartner): """Describes a withdrawal transaction with Fragment. diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index e00096e2388..9b527ef2b05 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -4469,7 +4469,7 @@ async def get_available_gifts( async def send_gift( self, user_id: Optional[int] = None, - gift_id: Union[str, Gift] = None, + gift_id: Union[str, Gift] = None, # type: ignore text: Optional[str] = None, text_parse_mode: ODVInput[str] = DEFAULT_NONE, text_entities: Optional[Sequence["MessageEntity"]] = None, @@ -4485,6 +4485,7 @@ async def send_gift( ) -> bool: return await super().send_gift( user_id=user_id, + chat_id=chat_id, gift_id=gift_id, text=text, text_parse_mode=text_parse_mode, From a7c8f3358d96bb0052e12b5dcd6333a66f88c002 Mon Sep 17 00:00:00 2001 From: poolitzer Date: Sun, 16 Feb 2025 11:31:04 +0100 Subject: [PATCH 08/20] found way to convince bot_official to leave me alone --- tests/test_official/exceptions.py | 52 ++++++++++++++++--------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index f4fd5de33c4..6578024ff41 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -19,7 +19,7 @@ """This module contains exceptions to our API compared to the official API.""" import datetime as dtm -from telegram import Animation, Audio, Document, Gift, PhotoSize, Sticker, Video, VideoNote, Voice +from telegram import Animation, Audio, Document, PhotoSize, Sticker, Video, VideoNote, Voice from tests.test_official.helpers import _get_params_base IGNORED_OBJECTS = ("ResponseParameters",) @@ -47,7 +47,8 @@ class ParamTypeCheckingExceptions: "animation": Animation, "voice": Voice, "sticker": Sticker, - "gift_id": Gift, + # TODO: Deprecated and will be corrected (and readded) in next major PTB version: + # "gift_id": Gift, }, "(delete|set)_sticker.*": { "sticker$": Sticker, @@ -79,29 +80,30 @@ class ParamTypeCheckingExceptions: # Special cases for other parameters that accept more types than the official API, and are # too complex to compare/predict with official API # structure: class/method_name: {param_name: reduced form of annotation} - COMPLEX_TYPES = ( - { # (param_name, is_class (i.e appears in a class?)): reduced form of annotation - "send_poll": {"correct_option_id": int}, # actual: Literal - "get_file": { - "file_id": str, # actual: Union[str, objs_with_file_id_attr] - }, - r"\w+invite_link": { - "invite_link": str, # actual: Union[str, ChatInviteLink] - }, - "send_invoice|create_invoice_link": { - "provider_data": str, # actual: Union[str, obj] - }, - "InlineKeyboardButton": { - "callback_data": str, # actual: Union[str, obj] - }, - "Input(Paid)?Media.*": { - "media": str, # actual: Union[str, InputMedia*, FileInput] - }, - "EncryptedPassportElement": { - "data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress] - }, - } - ) + COMPLEX_TYPES = { + "send_poll": {"correct_option_id": int}, # actual: Literal + "get_file": { + "file_id": str, # actual: Union[str, objs_with_file_id_attr] + }, + r"\w+invite_link": { + "invite_link": str, # actual: Union[str, ChatInviteLink] + }, + "send_invoice|create_invoice_link": { + "provider_data": str, # actual: Union[str, obj] + }, + "InlineKeyboardButton": { + "callback_data": str, # actual: Union[str, obj] + }, + "Input(Paid)?Media.*": { + "media": str, # actual: Union[str, InputMedia*, FileInput] + }, + "EncryptedPassportElement": { + "data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress] + }, + # TODO: Deprecated and will be corrected (and removed) in next major PTB + # version: + "send_gift": {"gift_id": str}, # actual: Non optional + } # param names ignored in the param type checking in classes for the `tg.Defaults` case. IGNORED_DEFAULTS_PARAM_NAMES = { From 901006c58b6df854b66c731638d5ada32d8342d1 Mon Sep 17 00:00:00 2001 From: poolitzer Date: Sun, 16 Feb 2025 11:45:25 +0100 Subject: [PATCH 09/20] Forgot to write the tests --- .../_payment/stars/test_transactionpartner.py | 61 +++++++++++++++++++ tests/test_gifts.py | 22 +++++++ 2 files changed, 83 insertions(+) diff --git a/tests/_payment/stars/test_transactionpartner.py b/tests/_payment/stars/test_transactionpartner.py index 02db851e6b8..75c1cb6982b 100644 --- a/tests/_payment/stars/test_transactionpartner.py +++ b/tests/_payment/stars/test_transactionpartner.py @@ -22,12 +22,14 @@ from telegram import ( AffiliateInfo, + Chat, Gift, PaidMediaVideo, RevenueWithdrawalStatePending, Sticker, TransactionPartner, TransactionPartnerAffiliateProgram, + TransactionPartnerChat, TransactionPartnerFragment, TransactionPartnerOther, TransactionPartnerTelegramAds, @@ -95,6 +97,10 @@ class TransactionPartnerTestBase: amount=42, ) request_count = 42 + chat = Chat( + id=3, + type=Chat.CHANNEL, + ) class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase): @@ -123,6 +129,7 @@ def test_de_json(self, offline_bot): ("telegram_ads", TransactionPartnerTelegramAds), ("telegram_api", TransactionPartnerTelegramApi), ("other", TransactionPartnerOther), + ("chat", TransactionPartnerChat), ], ) def test_subclass(self, offline_bot, tp_type, subclass): @@ -450,3 +457,57 @@ def test_equality(self, transaction_partner_telegram_api): assert a != d assert hash(a) != hash(d) + + +@pytest.fixture +def transaction_partner_chat(): + return TransactionPartnerChat( + chat=TransactionPartnerTestBase.chat, + gift=TransactionPartnerTestBase.gift, + ) + + +class TestTransactionPartnerChatWithoutRequest(TransactionPartnerTestBase): + type = TransactionPartnerType.CHAT + + def test_slot_behaviour(self, transaction_partner_chat): + inst = transaction_partner_chat + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "chat": self.chat.to_dict(), + "gift": self.gift.to_dict(), + } + tp = TransactionPartnerChat.de_json(json_dict, offline_bot) + assert tp.api_kwargs == {} + assert tp.type == "chat" + assert tp.chat == self.chat + assert tp.gift == self.gift + + def test_to_dict(self, transaction_partner_chat): + json_dict = transaction_partner_chat.to_dict() + assert json_dict["type"] == self.type + assert json_dict["chat"] == self.chat.to_dict() + assert json_dict["gift"] == self.gift.to_dict() + + def test_equality(self, transaction_partner_chat): + a = transaction_partner_chat + b = TransactionPartnerChat( + chat=self.chat, + ) + c = TransactionPartnerChat( + chat=Chat(id=1, type=Chat.CHANNEL), + ) + d = Chat(id=1, type=Chat.CHANNEL) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/tests/test_gifts.py b/tests/test_gifts.py index f350af95991..b5314b83dbb 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -164,6 +164,28 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): pay_for_upgrade=True, ) + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + user_id = request_data.parameters["chat_id"] == "chat_id" + gift_id = request_data.parameters["gift_id"] == "gift_id" + text = request_data.parameters["text"] == "text" + text_parse_mode = request_data.parameters["text_parse_mode"] == "text_parse_mode" + tes = request_data.parameters["text_entities"] == [ + me.to_dict() for me in text_entities + ] + pay_for_upgrade = request_data.parameters["pay_for_upgrade"] is True + + return user_id and gift_id and text and text_parse_mode and tes and pay_for_upgrade + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.send_gift( + gift_id=gift, + text="text", + text_parse_mode="text_parse_mode", + text_entities=text_entities, + pay_for_upgrade=True, + chat_id="chat_id", + ) + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) @pytest.mark.parametrize( ("passed_value", "expected_value"), From dd907e405b42f4e021ab133de2945b16020bb720 Mon Sep 17 00:00:00 2001 From: poolitzer Date: Sun, 16 Feb 2025 11:53:26 +0100 Subject: [PATCH 10/20] And for that line as well --- tests/test_gifts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_gifts.py b/tests/test_gifts.py index b5314b83dbb..ed5c9afce56 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -185,6 +185,8 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): pay_for_upgrade=True, chat_id="chat_id", ) + with pytest.raises(TypeError, match="Missing required argument `gift_id`."): + await offline_bot.send_gift() @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) @pytest.mark.parametrize( From c24724657592b298803216bf366096da37099a13 Mon Sep 17 00:00:00 2001 From: poolitzer Date: Fri, 21 Feb 2025 09:31:36 +0100 Subject: [PATCH 11/20] Fix: Apply review comments --- telegram/_chat.py | 3 +-- tests/auxil/bot_method_checks.py | 1 - tests/test_chat.py | 1 + tests/test_gifts.py | 10 ++++++++++ tests/test_official/exceptions.py | 6 +++--- tests/test_user.py | 2 ++ 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/telegram/_chat.py b/telegram/_chat.py index 6fd1d59427d..e615f1b9b6b 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -3481,8 +3481,6 @@ async def send_gift( :obj:`bool`: On success, :obj:`True` is returned. """ return await self.get_bot().send_gift( - chat_id=self.id if self.type == Chat.CHANNEL else None, - user_id=self.id if self.type == Chat.PRIVATE else None, gift_id=gift_id, text=text, text_parse_mode=text_parse_mode, @@ -3493,6 +3491,7 @@ async def send_gift( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + **{"chat_id" if self.type == Chat.CHANNEL else "user_id": self.id}, ) async def verify( diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 657647d18d5..610f49478ac 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -653,7 +653,6 @@ async def check_defaults_handling( expected_defaults_value=expected_defaults_value, ) request.post = assertion_callback - print(kwargs) await method(**kwargs) # 2: test that we get the manually passed non-None value diff --git a/tests/test_chat.py b/tests/test_chat.py index 39c427fe275..54e7d29255e 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1331,6 +1331,7 @@ async def make_assertion_channel(*_, **kwargs): ) # TODO discuss if better way exists + # tags: deprecated NEXT.VERSION with pytest.raises( Exception, match="Default for argument gift_id does not match the default of the Bot method.", diff --git a/tests/test_gifts.py b/tests/test_gifts.py index ed5c9afce56..c545949102a 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -164,6 +164,16 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): pay_for_upgrade=True, ) + async def test_send_gift_without_gift_id(self, offline_bot, gift, monkeypatch): + # Only here because we have to temporarily mark gift_id as optional. + # tags: deprecated NEXT.VERSION + + # We can't send actual gifts, so we just check that the correct parameters are passed + text_entities = [ + MessageEntity(MessageEntity.TEXT_LINK, 0, 4, "url"), + MessageEntity(MessageEntity.BOLD, 5, 9), + ] + async def make_assertion(url, request_data: RequestData, *args, **kwargs): user_id = request_data.parameters["chat_id"] == "chat_id" gift_id = request_data.parameters["gift_id"] == "gift_id" diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 6578024ff41..fab6bf2861c 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -47,7 +47,7 @@ class ParamTypeCheckingExceptions: "animation": Animation, "voice": Voice, "sticker": Sticker, - # TODO: Deprecated and will be corrected (and readded) in next major PTB version: + # TODO: Deprecated and will be corrected (and readded) in next major bot API release: # "gift_id": Gift, }, "(delete|set)_sticker.*": { @@ -73,7 +73,7 @@ class ParamTypeCheckingExceptions: ("keyboard", True): "KeyboardButton", # + sequence[sequence[str]] ("reaction", False): "ReactionType", # + str ("options", False): "InputPollOption", # + str - # TODO: Deprecated and will be corrected (and removed) in next major PTB version: + # TODO: Deprecated and will be corrected (and removed) in next bot api release ("file_hashes", True): "list[str]", } @@ -200,7 +200,7 @@ def ptb_ignored_params(object_name: str) -> set[str]: "send_venue": {"latitude", "longitude", "title", "address"}, "send_contact": {"phone_number", "first_name"}, # ----> - # here for backwards compatibility + # here for backwards compatibility. Todo: remove on next bot api release "send_gift": {"gift_id"}, } diff --git a/tests/test_user.py b/tests/test_user.py index ae16798c42e..f786a3a7da7 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -731,6 +731,8 @@ async def make_assertion(*_, **kwargs): and kwargs["text_entities"] == "text_entities" ) + # TODO discuss if better way exists + # tags: deprecated NEXT.VERSION with pytest.raises( Exception, match="Default for argument gift_id does not match the default of the Bot method.", From 8978eed29e01fdb6a405abf3e809bb8a01318ae2 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:17:36 +0100 Subject: [PATCH 12/20] Fix a failing test --- tests/test_gifts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gifts.py b/tests/test_gifts.py index c545949102a..ea5cde2d55c 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -176,7 +176,7 @@ async def test_send_gift_without_gift_id(self, offline_bot, gift, monkeypatch): async def make_assertion(url, request_data: RequestData, *args, **kwargs): user_id = request_data.parameters["chat_id"] == "chat_id" - gift_id = request_data.parameters["gift_id"] == "gift_id" + gift_id = request_data.parameters["gift_id"] == "some_id" text = request_data.parameters["text"] == "text" text_parse_mode = request_data.parameters["text_parse_mode"] == "text_parse_mode" tes = request_data.parameters["text_entities"] == [ From 54495f2a4ad3d09d636717377456182a16b868e9 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:08:17 +0100 Subject: [PATCH 13/20] Copilot review --- tests/_payment/stars/test_transactionpartner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/_payment/stars/test_transactionpartner.py b/tests/_payment/stars/test_transactionpartner.py index 75c1cb6982b..3f795b93ca2 100644 --- a/tests/_payment/stars/test_transactionpartner.py +++ b/tests/_payment/stars/test_transactionpartner.py @@ -497,6 +497,7 @@ def test_equality(self, transaction_partner_chat): a = transaction_partner_chat b = TransactionPartnerChat( chat=self.chat, + gift=self.gift, ) c = TransactionPartnerChat( chat=Chat(id=1, type=Chat.CHANNEL), From aadbb273ab208f1837b50f2c0ce8134dc535bae6 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:15:33 +0100 Subject: [PATCH 14/20] update tests a bit --- tests/test_gifts.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_gifts.py b/tests/test_gifts.py index ea5cde2d55c..3d4b064c8a4 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -164,7 +164,8 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): pay_for_upgrade=True, ) - async def test_send_gift_without_gift_id(self, offline_bot, gift, monkeypatch): + @pytest.mark.parametrize("id_name", ["user_id", "chat_id"]) + async def test_send_gift_user_chat_id(self, offline_bot, gift, monkeypatch, id_name): # Only here because we have to temporarily mark gift_id as optional. # tags: deprecated NEXT.VERSION @@ -175,7 +176,7 @@ async def test_send_gift_without_gift_id(self, offline_bot, gift, monkeypatch): ] async def make_assertion(url, request_data: RequestData, *args, **kwargs): - user_id = request_data.parameters["chat_id"] == "chat_id" + received_id = request_data.parameters[id_name] == id_name gift_id = request_data.parameters["gift_id"] == "some_id" text = request_data.parameters["text"] == "text" text_parse_mode = request_data.parameters["text_parse_mode"] == "text_parse_mode" @@ -184,7 +185,7 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): ] pay_for_upgrade = request_data.parameters["pay_for_upgrade"] is True - return user_id and gift_id and text and text_parse_mode and tes and pay_for_upgrade + return received_id and gift_id and text and text_parse_mode and tes and pay_for_upgrade monkeypatch.setattr(offline_bot.request, "post", make_assertion) assert await offline_bot.send_gift( @@ -193,8 +194,10 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): text_parse_mode="text_parse_mode", text_entities=text_entities, pay_for_upgrade=True, - chat_id="chat_id", + **{id_name: id_name}, ) + + async def test_send_gift_without_gift_id(self, offline_bot): with pytest.raises(TypeError, match="Missing required argument `gift_id`."): await offline_bot.send_gift() From 9912cd17709791e55d4863cc65487e7736b1ff0d Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:33:14 +0200 Subject: [PATCH 15/20] Api 8.3 Cover & Start Timestamp (#4682) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- telegram/_bot.py | 23 +++++++++++++++ telegram/_callbackquery.py | 2 ++ telegram/_chat.py | 12 ++++++++ telegram/_files/inputmedia.py | 52 ++++++++++++++++++++++++++++++++- telegram/_files/video.py | 44 ++++++++++++++++++++++++++-- telegram/_message.py | 10 +++++++ telegram/_user.py | 12 ++++++++ telegram/ext/_extbot.py | 8 +++++ tests/_files/test_inputmedia.py | 26 +++++++++++++++-- tests/_files/test_video.py | 17 ++++++++++- tests/test_bot.py | 2 ++ 11 files changed, 202 insertions(+), 6 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index f8b53211ed8..ca3d108451a 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -1218,6 +1218,7 @@ async def forward_message( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + video_start_timestamp: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1242,6 +1243,10 @@ async def forward_message( original message was sent (or channel username in the format ``@channelusername``). message_id (:obj:`int`): Message identifier in the chat specified in :paramref:`from_chat_id`. + video_start_timestamp (:obj:`int`, optional): New start timestamp for the + forwarded video in the message + + .. versionadded:: NEXT.VERSION disable_notification (:obj:`bool`, optional): |disable_notification| protect_content (:obj:`bool`, optional): |protect_content| @@ -1260,6 +1265,7 @@ async def forward_message( "chat_id": chat_id, "from_chat_id": from_chat_id, "message_id": message_id, + "video_start_timestamp": video_start_timestamp, } return await self._send_message( @@ -1955,6 +1961,8 @@ async def send_video( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + cover: Optional[FileInput] = None, + start_timestamp: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -2002,6 +2010,13 @@ async def send_video( |time-period-input| width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. + cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): Cover for the video in the message. |fileinputnopath| + + .. versionadded:: NEXT.VERSION + start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message. + + .. versionadded:: NEXT.VERSION caption (:obj:`str`, optional): Video caption (may also be used when resending videos by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -2088,6 +2103,8 @@ async def send_video( "width": width, "height": height, "supports_streaming": supports_streaming, + "cover": self._parse_file_input(cover, attach=True) if cover else None, + "start_timestamp": start_timestamp, "thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None, "has_spoiler": has_spoiler, "show_caption_above_media": show_caption_above_media, @@ -7974,6 +7991,7 @@ async def copy_message( reply_parameters: Optional["ReplyParameters"] = None, show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, + video_start_timestamp: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -7993,6 +8011,10 @@ async def copy_message( from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the original message was sent (or channel username in the format ``@channelusername``). message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id. + video_start_timestamp (:obj:`int`, optional): New start timestamp for the + copied video in the message + + .. versionadded:: NEXT.VERSION caption (:obj:`str`, optional): New caption for media, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. If not specified, the original caption is kept. @@ -8083,6 +8105,7 @@ async def copy_message( "reply_parameters": reply_parameters, "show_caption_above_media": show_caption_above_media, "allow_paid_broadcast": allow_paid_broadcast, + "video_start_timestamp": video_start_timestamp, } result = await self._post( diff --git a/telegram/_callbackquery.py b/telegram/_callbackquery.py index c1ba637546f..99b4ad115b5 100644 --- a/telegram/_callbackquery.py +++ b/telegram/_callbackquery.py @@ -831,6 +831,7 @@ async def copy_message( reply_parameters: Optional["ReplyParameters"] = None, show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, + video_start_timestamp: Optional[int] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -864,6 +865,7 @@ async def copy_message( chat_id=chat_id, caption=caption, parse_mode=parse_mode, + video_start_timestamp=video_start_timestamp, caption_entities=caption_entities, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, diff --git a/telegram/_chat.py b/telegram/_chat.py index e615f1b9b6b..f5203a2e1c3 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -1940,6 +1940,8 @@ async def send_video( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + cover: Optional[FileInput] = None, + start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1978,6 +1980,8 @@ async def send_video( parse_mode=parse_mode, supports_streaming=supports_streaming, thumbnail=thumbnail, + cover=cover, + start_timestamp=start_timestamp, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, @@ -2199,6 +2203,7 @@ async def send_copy( reply_parameters: Optional["ReplyParameters"] = None, show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, + video_start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2225,6 +2230,7 @@ async def send_copy( from_chat_id=from_chat_id, message_id=message_id, caption=caption, + video_start_timestamp=video_start_timestamp, parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, @@ -2257,6 +2263,7 @@ async def copy_message( reply_parameters: Optional["ReplyParameters"] = None, show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, + video_start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2283,6 +2290,7 @@ async def copy_message( chat_id=chat_id, message_id=message_id, caption=caption, + video_start_timestamp=video_start_timestamp, parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, @@ -2398,6 +2406,7 @@ async def forward_from( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + video_start_timestamp: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2423,6 +2432,7 @@ async def forward_from( chat_id=self.id, from_chat_id=from_chat_id, message_id=message_id, + video_start_timestamp=video_start_timestamp, disable_notification=disable_notification, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2440,6 +2450,7 @@ async def forward_to( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + video_start_timestamp: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2466,6 +2477,7 @@ async def forward_to( from_chat_id=self.id, chat_id=chat_id, message_id=message_id, + video_start_timestamp=video_start_timestamp, disable_notification=disable_notification, read_timeout=read_timeout, write_timeout=write_timeout, diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index 0ac17c2d55d..912769758e7 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.py @@ -214,6 +214,13 @@ class InputPaidMediaVideo(InputPaidMedia): Lastly you can pass an existing :class:`telegram.Video` object to send. thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ optional): |thumbdocstringnopath| + cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): Cover for the video in the message. |fileinputnopath| + + .. versionchanged:: NEXT.VERSION + start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message + + .. versionchanged:: NEXT.VERSION width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. duration (:obj:`int`, optional): Video duration in seconds. @@ -225,6 +232,13 @@ class InputPaidMediaVideo(InputPaidMedia): :tg-const:`telegram.constants.InputPaidMediaType.VIDEO`. media (:obj:`str` | :class:`telegram.InputFile`): Video to send. thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| + cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message. + |fileinputnopath| + + .. versionchanged:: NEXT.VERSION + start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message + + .. versionchanged:: NEXT.VERSION width (:obj:`int`): Optional. Video width. height (:obj:`int`): Optional. Video height. duration (:obj:`int`): Optional. Video duration in seconds. @@ -232,7 +246,15 @@ class InputPaidMediaVideo(InputPaidMedia): suitable for streaming. """ - __slots__ = ("duration", "height", "supports_streaming", "thumbnail", "width") + __slots__ = ( + "cover", + "duration", + "height", + "start_timestamp", + "supports_streaming", + "thumbnail", + "width", + ) def __init__( self, @@ -242,6 +264,8 @@ def __init__( height: Optional[int] = None, duration: Optional[int] = None, supports_streaming: Optional[bool] = None, + cover: Optional[FileInput] = None, + start_timestamp: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -264,6 +288,10 @@ def __init__( self.height: Optional[int] = height self.duration: Optional[int] = duration self.supports_streaming: Optional[bool] = supports_streaming + self.cover: Optional[Union[InputFile, str]] = ( + parse_file_input(cover, attach=True, local_mode=True) if cover else None + ) + self.start_timestamp: Optional[int] = start_timestamp class InputMediaAnimation(InputMedia): @@ -536,6 +564,13 @@ class InputMediaVideo(InputMedia): optional): |thumbdocstringnopath| .. versionadded:: 20.2 + cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): Cover for the video in the message. |fileinputnopath| + + .. versionchanged:: NEXT.VERSION + start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message + + .. versionchanged:: NEXT.VERSION show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| .. versionadded:: 21.3 @@ -568,13 +603,22 @@ class InputMediaVideo(InputMedia): show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| .. versionadded:: 21.3 + cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message. + |fileinputnopath| + + .. versionchanged:: NEXT.VERSION + start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message + + .. versionchanged:: NEXT.VERSION """ __slots__ = ( + "cover", "duration", "has_spoiler", "height", "show_caption_above_media", + "start_timestamp", "supports_streaming", "thumbnail", "width", @@ -594,6 +638,8 @@ def __init__( has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, show_caption_above_media: Optional[bool] = None, + cover: Optional[FileInput] = None, + start_timestamp: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -625,6 +671,10 @@ def __init__( self.supports_streaming: Optional[bool] = supports_streaming self.has_spoiler: Optional[bool] = has_spoiler self.show_caption_above_media: Optional[bool] = show_caption_above_media + self.cover: Optional[Union[InputFile, str]] = ( + parse_file_input(cover, attach=True, local_mode=True) if cover else None + ) + self.start_timestamp: Optional[int] = start_timestamp class InputMediaAudio(InputMedia): diff --git a/telegram/_files/video.py b/telegram/_files/video.py index c5ca6f3b946..37a87e77136 100644 --- a/telegram/_files/video.py +++ b/telegram/_files/video.py @@ -17,12 +17,17 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Video.""" -from typing import Optional +from collections.abc import Sequence +from typing import TYPE_CHECKING, Optional from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files.photosize import PhotoSize +from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg from telegram._utils.types import JSONDict +if TYPE_CHECKING: + from telegram import Bot + class Video(_BaseThumbedMedium): """This object represents a video file. @@ -48,6 +53,13 @@ class Video(_BaseThumbedMedium): thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail. .. versionadded:: 20.2 + cover (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the cover of + the video in the message. + + .. versionadded:: NEXT.VERSION + start_timestamp (:obj:`int`, optional): Timestamp in seconds from which the video + will play in the message + .. versionadded:: NEXT.VERSION Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download @@ -64,9 +76,24 @@ class Video(_BaseThumbedMedium): thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail. .. versionadded:: 20.2 + cover (tuple[:class:`telegram.PhotoSize`]): Optional, Available sizes of the cover of + the video in the message. + + .. versionadded:: NEXT.VERSION + start_timestamp (:obj:`int`): Optional, Timestamp in seconds from which the video + will play in the message + .. versionadded:: NEXT.VERSION """ - __slots__ = ("duration", "file_name", "height", "mime_type", "width") + __slots__ = ( + "cover", + "duration", + "file_name", + "height", + "mime_type", + "start_timestamp", + "width", + ) def __init__( self, @@ -79,6 +106,8 @@ def __init__( file_size: Optional[int] = None, file_name: Optional[str] = None, thumbnail: Optional[PhotoSize] = None, + cover: Optional[Sequence[PhotoSize]] = None, + start_timestamp: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -97,3 +126,14 @@ def __init__( # Optional self.mime_type: Optional[str] = mime_type self.file_name: Optional[str] = file_name + self.cover: Optional[Sequence[PhotoSize]] = parse_sequence_arg(cover) + self.start_timestamp: Optional[int] = start_timestamp + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Video": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["cover"] = de_list_optional(data.get("cover"), PhotoSize, bot) + + return super().de_json(data=data, bot=bot) diff --git a/telegram/_message.py b/telegram/_message.py index e5e5ce7ed4f..d8b6d13db5b 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -2592,6 +2592,8 @@ async def reply_video( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + cover: Optional[FileInput] = None, + start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2661,6 +2663,8 @@ async def reply_video( message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + cover=cover, + start_timestamp=start_timestamp, business_connection_id=self.business_connection_id, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, @@ -3506,6 +3510,7 @@ async def forward( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + video_start_timestamp: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3540,6 +3545,7 @@ async def forward( chat_id=chat_id, from_chat_id=self.chat_id, message_id=self.message_id, + video_start_timestamp=video_start_timestamp, disable_notification=disable_notification, protect_content=protect_content, message_thread_id=message_thread_id, @@ -3563,6 +3569,7 @@ async def copy( reply_parameters: Optional["ReplyParameters"] = None, show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, + video_start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3593,6 +3600,7 @@ async def copy( from_chat_id=self.chat_id, message_id=self.message_id, caption=caption, + video_start_timestamp=video_start_timestamp, parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, @@ -3625,6 +3633,7 @@ async def reply_copy( reply_parameters: Optional["ReplyParameters"] = None, show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, + video_start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3675,6 +3684,7 @@ async def reply_copy( from_chat_id=from_chat_id, message_id=message_id, caption=caption, + video_start_timestamp=video_start_timestamp, parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, diff --git a/telegram/_user.py b/telegram/_user.py index d28892e4135..bff13077cf7 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -1328,6 +1328,8 @@ async def send_video( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + cover: Optional[FileInput] = None, + start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1369,6 +1371,8 @@ async def send_video( parse_mode=parse_mode, supports_streaming=supports_streaming, thumbnail=thumbnail, + cover=cover, + start_timestamp=start_timestamp, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, @@ -1708,6 +1712,7 @@ async def send_copy( reply_parameters: Optional["ReplyParameters"] = None, show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, + video_start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1735,6 +1740,7 @@ async def send_copy( from_chat_id=from_chat_id, message_id=message_id, caption=caption, + video_start_timestamp=video_start_timestamp, parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, @@ -1767,6 +1773,7 @@ async def copy_message( reply_parameters: Optional["ReplyParameters"] = None, show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, + video_start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1794,6 +1801,7 @@ async def copy_message( chat_id=chat_id, message_id=message_id, caption=caption, + video_start_timestamp=video_start_timestamp, parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, @@ -1909,6 +1917,7 @@ async def forward_from( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + video_start_timestamp: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1934,6 +1943,7 @@ async def forward_from( chat_id=self.id, from_chat_id=from_chat_id, message_id=message_id, + video_start_timestamp=video_start_timestamp, disable_notification=disable_notification, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1951,6 +1961,7 @@ async def forward_to( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + video_start_timestamp: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1977,6 +1988,7 @@ async def forward_to( from_chat_id=self.id, chat_id=chat_id, message_id=message_id, + video_start_timestamp=video_start_timestamp, disable_notification=disable_notification, read_timeout=read_timeout, write_timeout=write_timeout, diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 9b527ef2b05..b35f1b98c9e 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -815,6 +815,7 @@ async def copy_message( reply_parameters: Optional["ReplyParameters"] = None, show_caption_above_media: Optional[bool] = None, allow_paid_broadcast: Optional[bool] = None, + video_start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -831,6 +832,7 @@ async def copy_message( from_chat_id=from_chat_id, message_id=message_id, caption=caption, + video_start_timestamp=video_start_timestamp, parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, @@ -1752,6 +1754,7 @@ async def forward_message( disable_notification: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: Optional[int] = None, + video_start_timestamp: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1764,6 +1767,7 @@ async def forward_message( chat_id=chat_id, from_chat_id=from_chat_id, message_id=message_id, + video_start_timestamp=video_start_timestamp, disable_notification=disable_notification, protect_content=protect_content, message_thread_id=message_thread_id, @@ -3240,6 +3244,8 @@ async def send_video( message_effect_id: Optional[str] = None, allow_paid_broadcast: Optional[bool] = None, show_caption_above_media: Optional[bool] = None, + cover: Optional[FileInput] = None, + start_timestamp: Optional[int] = None, *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3270,6 +3276,8 @@ async def send_video( business_connection_id=business_connection_id, has_spoiler=has_spoiler, thumbnail=thumbnail, + cover=cover, + start_timestamp=start_timestamp, filename=filename, reply_parameters=reply_parameters, read_timeout=read_timeout, diff --git a/tests/_files/test_inputmedia.py b/tests/_files/test_inputmedia.py index 5aef4e62da4..b362411cbd8 100644 --- a/tests/_files/test_inputmedia.py +++ b/tests/_files/test_inputmedia.py @@ -58,6 +58,8 @@ def input_media_video(class_thumb_file): parse_mode=InputMediaVideoTestBase.parse_mode, caption_entities=InputMediaVideoTestBase.caption_entities, thumbnail=class_thumb_file, + cover=class_thumb_file, + start_timestamp=InputMediaVideoTestBase.start_timestamp, supports_streaming=InputMediaVideoTestBase.supports_streaming, has_spoiler=InputMediaVideoTestBase.has_spoiler, show_caption_above_media=InputMediaVideoTestBase.show_caption_above_media, @@ -130,6 +132,8 @@ def input_paid_media_video(class_thumb_file): return InputPaidMediaVideo( media=InputMediaVideoTestBase.media, thumbnail=class_thumb_file, + cover=class_thumb_file, + start_timestamp=InputMediaVideoTestBase.start_timestamp, width=InputMediaVideoTestBase.width, height=InputMediaVideoTestBase.height, duration=InputMediaVideoTestBase.duration, @@ -144,6 +148,7 @@ class InputMediaVideoTestBase: width = 3 height = 4 duration = 5 + start_timestamp = 3 parse_mode = "HTML" supports_streaming = True caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] @@ -169,6 +174,8 @@ def test_expected_values(self, input_media_video): assert input_media_video.caption_entities == tuple(self.caption_entities) assert input_media_video.supports_streaming == self.supports_streaming assert isinstance(input_media_video.thumbnail, InputFile) + assert isinstance(input_media_video.cover, InputFile) + assert input_media_video.start_timestamp == self.start_timestamp assert input_media_video.has_spoiler == self.has_spoiler assert input_media_video.show_caption_above_media == self.show_caption_above_media @@ -194,6 +201,8 @@ def test_to_dict(self, input_media_video): input_media_video_dict["show_caption_above_media"] == input_media_video.show_caption_above_media ) + assert input_media_video_dict["cover"] == input_media_video.cover + assert input_media_video_dict["start_timestamp"] == input_media_video.start_timestamp def test_with_video(self, video): # fixture found in test_video @@ -214,10 +223,13 @@ def test_with_video_file(self, video_file): def test_with_local_files(self): input_media_video = InputMediaVideo( - data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg") + data_file("telegram.mp4"), + thumbnail=data_file("telegram.jpg"), + cover=data_file("telegram.jpg"), ) assert input_media_video.media == data_file("telegram.mp4").as_uri() assert input_media_video.thumbnail == data_file("telegram.jpg").as_uri() + assert input_media_video.cover == data_file("telegram.jpg").as_uri() def test_type_enum_conversion(self): # Since we have a lot of different test classes for all the input media types, we test this @@ -565,6 +577,8 @@ def test_expected_values(self, input_paid_media_video): assert input_paid_media_video.duration == self.duration assert input_paid_media_video.supports_streaming == self.supports_streaming assert isinstance(input_paid_media_video.thumbnail, InputFile) + assert isinstance(input_paid_media_video.cover, InputFile) + assert input_paid_media_video.start_timestamp == self.start_timestamp def test_to_dict(self, input_paid_media_video): input_paid_media_video_dict = input_paid_media_video.to_dict() @@ -578,6 +592,11 @@ def test_to_dict(self, input_paid_media_video): == input_paid_media_video.supports_streaming ) assert input_paid_media_video_dict["thumbnail"] == input_paid_media_video.thumbnail + assert input_paid_media_video_dict["cover"] == input_paid_media_video.cover + assert ( + input_paid_media_video_dict["start_timestamp"] + == input_paid_media_video.start_timestamp + ) def test_with_video(self, video): # fixture found in test_video @@ -596,10 +615,13 @@ def test_with_video_file(self, video_file): def test_with_local_files(self): input_paid_media_video = InputPaidMediaVideo( - data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg") + data_file("telegram.mp4"), + thumbnail=data_file("telegram.jpg"), + cover=data_file("telegram.jpg"), ) assert input_paid_media_video.media == data_file("telegram.mp4").as_uri() assert input_paid_media_video.thumbnail == data_file("telegram.jpg").as_uri() + assert input_paid_media_video.cover == data_file("telegram.jpg").as_uri() @pytest.fixture(scope="module") diff --git a/tests/_files/test_video.py b/tests/_files/test_video.py index 65d7ee8c354..d4d87122576 100644 --- a/tests/_files/test_video.py +++ b/tests/_files/test_video.py @@ -46,6 +46,8 @@ class VideoTestBase: mime_type = "video/mp4" supports_streaming = True file_name = "telegram.mp4" + start_timestamp = 3 + cover = (PhotoSize("file_id", "unique_id", 640, 360, file_size=0),) thumb_width = 180 thumb_height = 320 thumb_file_size = 1767 @@ -92,6 +94,8 @@ def test_de_json(self, offline_bot): "mime_type": self.mime_type, "file_size": self.file_size, "file_name": self.file_name, + "start_timestamp": self.start_timestamp, + "cover": [photo_size.to_dict() for photo_size in self.cover], } json_video = Video.de_json(json_dict, offline_bot) assert json_video.api_kwargs == {} @@ -104,6 +108,8 @@ def test_de_json(self, offline_bot): assert json_video.mime_type == self.mime_type assert json_video.file_size == self.file_size assert json_video.file_name == self.file_name + assert json_video.start_timestamp == self.start_timestamp + assert json_video.cover == self.cover def test_to_dict(self, video): video_dict = video.to_dict() @@ -223,7 +229,9 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): class TestVideoWithRequest(VideoTestBase): @pytest.mark.parametrize("duration", [dtm.timedelta(seconds=5), 5]) - async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file, duration): + async def test_send_all_args( + self, bot, chat_id, video_file, video, thumb_file, photo_file, duration + ): message = await bot.send_video( chat_id, video_file, @@ -236,6 +244,8 @@ async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file, height=video.height, parse_mode="Markdown", thumbnail=thumb_file, + cover=photo_file, + start_timestamp=self.start_timestamp, has_spoiler=True, show_caption_above_media=True, ) @@ -256,6 +266,11 @@ async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file, assert message.video.thumbnail.width == self.thumb_width assert message.video.thumbnail.height == self.thumb_height + assert message.video.start_timestamp == self.start_timestamp + + assert isinstance(message.video.cover, tuple) + assert isinstance(message.video.cover[0], PhotoSize) + assert message.video.file_name == self.file_name assert message.has_protected_content assert message.has_media_spoiler diff --git a/tests/test_bot.py b/tests/test_bot.py index 2c9ba6abed0..29474ef2bed 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1617,6 +1617,7 @@ async def post(url, request_data: RequestData, *args, **kwargs): == [MessageEntity(MessageEntity.BOLD, 0, 4).to_dict()], data["protect_content"] is True, data["message_thread_id"] == 1, + data["video_start_timestamp"] == 999, ] ): pytest.fail("I got wrong parameters in post") @@ -1628,6 +1629,7 @@ async def post(url, request_data: RequestData, *args, **kwargs): from_chat_id=chat_id, message_id=media_message.message_id, caption=caption, + video_start_timestamp=999, caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 4)], parse_mode=ParseMode.HTML, reply_to_message_id=media_message.message_id, From e32ae4e96be09730b46a579fdf61900ae1c7cd84 Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:38:41 +0200 Subject: [PATCH 16/20] Add `InputMedia.thumbnail` to `test_official`'s complex types. (#4690) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- tests/test_official/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index fab6bf2861c..a6eb90dbd7e 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -96,6 +96,8 @@ class ParamTypeCheckingExceptions: }, "Input(Paid)?Media.*": { "media": str, # actual: Union[str, InputMedia*, FileInput] + # see also https://github.com/tdlib/telegram-bot-api/issues/707 + "thumbnail": str, # actual: Union[str, FileInput] }, "EncryptedPassportElement": { "data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress] From 58b0442166e7f20f5f0e1e31baef4796aa63e6f8 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:49:11 +0100 Subject: [PATCH 17/20] adapt test official --- tests/test_official/exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index a6eb90dbd7e..8fd54dddd82 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -98,6 +98,7 @@ class ParamTypeCheckingExceptions: "media": str, # actual: Union[str, InputMedia*, FileInput] # see also https://github.com/tdlib/telegram-bot-api/issues/707 "thumbnail": str, # actual: Union[str, FileInput] + "cover": str, # actual: Union[str, FileInput] }, "EncryptedPassportElement": { "data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress] From a1b05325daed6f8701d741b0825323eddf24d024 Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Thu, 27 Feb 2025 21:42:06 +0200 Subject: [PATCH 18/20] Update `constants.GiftLimit.MAX_TEXT_LENGTH` (#4691) Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- telegram/constants.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telegram/constants.py b/telegram/constants.py index 0414e05a0af..fc895f66e3b 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -1236,9 +1236,12 @@ class GiftLimit(IntEnum): __slots__ = () - MAX_TEXT_LENGTH = 255 + MAX_TEXT_LENGTH = 128 """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the :paramref:`~telegram.Bot.send_gift.text` parameter of :meth:`~telegram.Bot.send_gift`. + + .. versionchanged:: NEXT.VERSION + Updated Value to 128 based on Bot API 8.3 """ From 10dcef85f5fe7e86002bc1570819e1e39fe0d6d5 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:36:57 +0100 Subject: [PATCH 19/20] One doc update --- telegram/_bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 2e2dd0e6a17..e645738a5b9 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9300,7 +9300,8 @@ async def set_message_reaction( api_kwargs: Optional[JSONDict] = None, ) -> bool: """ - Use this method to change the chosen reactions on a message. Service messages can't be + Use this method to change the chosen reactions on a message. Service messages of some types + can't be reacted to. Automatically forwarded messages from a channel to its discussion group have the same available reactions as messages in the channel. Bots can't use paid reactions. From 38beaf11267ad43b1e6aad009ca201aa455c81d2 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:39:17 +0100 Subject: [PATCH 20/20] update bot api version --- README.rst | 4 ++-- telegram/constants.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 41b38d84fe6..d19a93d4d3f 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-8.2-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-8.3-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 **8.2** are natively supported by this library. +All types and methods of the Telegram Bot API **8.3** 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/telegram/constants.py b/telegram/constants.py index fc895f66e3b..aaee4f0ad15 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -155,7 +155,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=2) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=3) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`.