From 31a57a0af9b140e9e47478ead7fce72cf6854586 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 4 May 2025 15:39:29 +0300 Subject: [PATCH 1/8] Add method `Bot.gift_premium_subscription`. Telegram Premium - Added the method giftPremiumSubscription, allowing bots to gift a user a Telegram Premium subscription paid in Telegram Stars. --- docs/source/inclusions/bot_methods.rst | 2 + telegram/_bot.py | 79 ++++++++++++++++++++++++++ telegram/constants.py | 53 +++++++++++++++++ telegram/ext/_extbot.py | 31 ++++++++++ tests/test_bot.py | 55 ++++++++++++++++++ 5 files changed, 220 insertions(+) diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index eeb3d65955a..d1ff3c3ac13 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -394,6 +394,8 @@ - Used for obtaining the bot's Telegram Stars transactions * - :meth:`~telegram.Bot.refund_star_payment` - Used for refunding a payment in Telegram Stars + * - :meth:`~telegram.Bot.gift_premium_subscription` + - Used for gifting Telegram Premium to another user. .. raw:: html diff --git a/telegram/_bot.py b/telegram/_bot.py index 62a466cc4b7..c61b91ae082 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9368,6 +9368,83 @@ async def set_message_reaction( api_kwargs=api_kwargs, ) + async def gift_premium_subscription( + self, + user_id: int, + month_count: int, + star_count: int, + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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: + """ + Gifts a Telegram Premium subscription to the given user. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): Unique identifier of the target user who will receive a Telegram + Premium subscription. + month_count (:obj:`int`): Number of months the Telegram Premium subscription will be + active for the user; must be one of + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_THREE`, + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_SIX`, + or :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_TWELVE`. + star_count (:obj:`int`): Number of Telegram Stars to pay for the Telegram Premium + subscription; must be + :tg-const:`telegram.constants.PremiumSubscription.STARS_THREE_MONTHS` + for :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_THREE` months, + :tg-const:`telegram.constants.PremiumSubscription.STARS_SIX_MONTHS` + for :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_SIX` months, + and :tg-const:`telegram.constants.PremiumSubscription.STARS_TWELVE_MONTHS` + for :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_TWELVE` months. + text (:obj:`str`, optional): Text that will be shown along with the service message + about the subscription; + 0-:tg-const:`telegram.constants.PremiumSubscription.MAX_TEXT_LENGTH` characters. + text_parse_mode (:obj:`str`, optional): Mode for parsing entities. + See :class:`telegram.constants.ParseMode` and + `formatting options `__ for + more details. Entities other than :attr:`~MessageEntity.BOLD`, + :attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`, + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and + :attr:`~MessageEntity.CUSTOM_EMOJI` are ignored. + text_entities (Sequence[:class:`telegram.MessageEntity`], optional): A list of special + entities that appear in the gift text. It can be specified instead of + :paramref:`text_parse_mode`. Entities other than :attr:`~MessageEntity.BOLD`, + :attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`, + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and + :attr:`~MessageEntity.CUSTOM_EMOJI` are ignored. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + "month_count": month_count, + "star_count": star_count, + "text": text, + "text_entities": text_entities, + "text_parse_mode": text_parse_mode, + } + return await self._post( + "giftPremiumSubscription", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def get_business_connection( self, business_connection_id: str, @@ -11236,6 +11313,8 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`get_user_chat_boosts`""" setMessageReaction = set_message_reaction """Alias for :meth:`set_message_reaction`""" + giftPremiumSubscription = gift_premium_subscription + """Alias for :meth:`gift_premium_subscription`""" getBusinessAccountGifts = get_business_account_gifts """Alias for :meth:`get_business_account_gifts`""" getBusinessAccountStarBalance = get_business_account_star_balance diff --git a/telegram/constants.py b/telegram/constants.py index 4e3ac0ab144..1d1f2822774 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -97,6 +97,7 @@ "PollLimit", "PollType", "PollingLimit", + "PremiumSubscription", "ProfileAccentColor", "ReactionEmoji", "ReactionType", @@ -2321,6 +2322,58 @@ class PollingLimit(IntEnum): """ +class PremiumSubscription(IntEnum): + """This enum contains limitations for :meth:`~telegram.Bot.gift_premium_subscription`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MAX_TEXT_LENGTH = 128 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.gift_premium_subscription.text` + parameter of :meth:`~telegram.Bot.gift_premium_subscription`. + """ + MONTH_COUNT_THREE = 3 + """:obj:`int`: Possible value for + :paramref:`~telegram.Bot.gift_premium_subscription.month_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`; number of months the Premium + subscription will be active for. + """ + MONTH_COUNT_SIX = 6 + """:obj:`int`: Possible value for + :paramref:`~telegram.Bot.gift_premium_subscription.month_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`; number of months the Premium + subscription will be active for. + """ + MONTH_COUNT_TWELVE = 12 + """:obj:`int`: Possible value for + :paramref:`~telegram.Bot.gift_premium_subscription.month_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`; number of months the Premium + subscription will be active for. + """ + STARS_THREE_MONTHS = 1000 + """:obj:`int`: Number of Telegram Stars to pay for a Premium subscription of + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_THREE` months period. + Relevant for :paramref:`~telegram.Bot.gift_premium_subscription.star_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`. + """ + STARS_SIX_MONTHS = 1500 + """:obj:`int`: Number of Telegram Stars to pay for a Premium subscription of + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_SIX` months period. + Relevant for :paramref:`~telegram.Bot.gift_premium_subscription.star_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`. + """ + STARS_TWELVE_MONTHS = 2500 + """:obj:`int`: Number of Telegram Stars to pay for a Premium subscription of + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_TWELVE` months period. + Relevant for :paramref:`~telegram.Bot.gift_premium_subscription.star_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`. + """ + + class ProfileAccentColor(Enum): """This enum contains the available accent colors for :class:`telegram.ChatFullInfo.profile_accent_color_id`. diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index fd3eb41285b..801d4b0d179 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -4253,6 +4253,36 @@ async def set_message_reaction( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def gift_premium_subscription( + self, + user_id: int, + month_count: int, + star_count: int, + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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().gift_premium_subscription( + user_id=user_id, + month_count=month_count, + star_count=star_count, + text=text, + text_parse_mode=text_parse_mode, + text_entities=text_entities, + 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 get_business_connection( self, business_connection_id: str, @@ -5148,6 +5178,7 @@ async def remove_user_verification( unpinAllGeneralForumTopicMessages = unpin_all_general_forum_topic_messages getUserChatBoosts = get_user_chat_boosts setMessageReaction = set_message_reaction + giftPremiumSubscription = gift_premium_subscription getBusinessConnection = get_business_connection getBusinessAccountGifts = get_business_account_gifts getBusinessAccountStarBalance = get_business_account_star_balance diff --git a/tests/test_bot.py b/tests/test_bot.py index 611a8462859..16c878dd29c 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2385,6 +2385,61 @@ async def make_assertion(*args, **_): monkeypatch.setattr(bot, "_post", make_assertion) assert await bot.send_chat_action(chat_id, "action", 1, 3) + async def test_gift_premium_subscription_all_args(self, bot, monkeypatch): + # can't make actual request so we just test that the correct data is passed + async def make_assertion(*args, **_): + kwargs = args[1] + return ( + kwargs.get("user_id") == 12 + and kwargs.get("month_count") == 3 + and kwargs.get("star_count") == 1000 + and kwargs.get("text") == "test text" + and kwargs.get("text_parse_mode") == "Markdown" + and kwargs.get("text_entities") + == [ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 11), + ] + ) + + monkeypatch.setattr(bot, "_post", make_assertion) + assert await bot.gift_premium_subscription( + user_id=12, + month_count=3, + star_count=1000, + text="test text", + text_parse_mode="Markdown", + text_entities=[ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 11), + ], + ) + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, "Markdown"), ("HTML", "HTML"), (None, None)], + ) + async def test_gift_premium_subscription_default_parse_mode( + self, default_bot, monkeypatch, passed_value, expected_value + ): + # can't make actual request so we just test that the correct data is passed + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("text_parse_mode") == expected_value + return True + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "user_id": 123, + "month_count": 3, + "star_count": 1000, + "text": "text", + } + if passed_value is not DEFAULT_NONE: + kwargs["text_parse_mode"] = passed_value + + assert await default_bot.gift_premium_subscription(**kwargs) + async def test_refund_star_payment(self, offline_bot, monkeypatch): # can't make actual request so we just test that the correct data is passed async def make_assertion(url, request_data: RequestData, *args, **kwargs): From e25479caa5e785631dfe2f9f833bf5b0dcc91be1 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 4 May 2025 17:49:34 +0300 Subject: [PATCH 2/8] Add new fields to `TransactionPartnerUser`. Telegram Premium - Added the field premium_subscription_duration to the class TransactionPartnerUser for transactions involving a Telegram Premium subscription purchased by the bot. - Added the field transaction_type to the class TransactionPartnerUser, simplifying the differentiation and processing of all transaction types. --- telegram/_payment/stars/transactionpartner.py | 88 ++++++++++++++++--- telegram/constants.py | 33 +++++++ tests/_payment/stars/test_startransactions.py | 2 + .../_payment/stars/test_transactionpartner.py | 17 ++++ tests/test_official/exceptions.py | 1 + 5 files changed, 128 insertions(+), 13 deletions(-) diff --git a/telegram/_payment/stars/transactionpartner.py b/telegram/_payment/stars/transactionpartner.py index 811947581ee..6ac795518fb 100644 --- a/telegram/_payment/stars/transactionpartner.py +++ b/telegram/_payment/stars/transactionpartner.py @@ -281,55 +281,107 @@ class TransactionPartnerUser(TransactionPartner): """Describes a transaction with a user. Objects of this class are comparable in terms of equality. Two objects of this class are - considered equal, if their :attr:`user` are equal. + considered equal, if their :attr:`user` and :attr:`transaction_type` are equal. .. versionadded:: 21.4 + .. versionchanged:: NEXT.VERSION + Equality comparison now includes the new required argument :paramref:`transaction_type`, + introduced in Bot API 9.0. + Args: + transaction_type (:obj:`str`): Type of the transaction, currently one of + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` for payments via + invoices, :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + for payments for paid media, + :tg-const:`telegram.constants.TransactionPartnerUser.GIFT_PURCHASE` for gifts sent by + the bot, :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` + for Telegram Premium subscriptions gifted by the bot, + :tg-const:`telegram.constants.TransactionPartnerUser.BUSINESS_ACCOUNT_TRANSFER` for + direct transfers from managed business accounts. user (:class:`telegram.User`): Information about the user. affiliate (:class:`telegram.AffiliateInfo`, optional): Information about the affiliate that - received a commission via this transaction + received a commission via this transaction. Can be available only for + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` + and :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + transactions. .. versionadded:: 21.9 - invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. + invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. Can be available + only for :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` + transactions. subscription_period (:class:`datetime.timedelta`, optional): The duration of the paid - subscription + subscription. Can be available only for + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` transactions. .. versionadded:: 21.8 paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid - media bought by the user. + media bought by the user. for + :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + transactions only. .. versionadded:: 21.5 - paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload. + paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload. Can be + available only for + :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` transactions. .. versionadded:: 21.6 - gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot + gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot; for + :tg-const:`telegram.constants.TransactionPartnerUser.GIFT_PURCHASE` transactions only. .. versionadded:: 21.8 + premium_subscription_duration (:obj:`int`, optional): Number of months the gifted Telegram + Premium subscription will be active for; for + :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` + transactions only. + + .. versionadded:: NEXT.VERSION Attributes: type (:obj:`str`): The type of the transaction partner, always :tg-const:`telegram.TransactionPartner.USER`. + transaction_type (:obj:`str`): Type of the transaction, currently one of “invoice_payment” + for payments via invoices, “paid_media_payment” for payments for paid media, + “gift_purchase” for gifts sent by the bot, “premium_purchase” for Telegram Premium + subscriptions gifted by the bot, “business_account_transfer” for direct transfers from + managed business accounts. user (:class:`telegram.User`): Information about the user. affiliate (:class:`telegram.AffiliateInfo`): Optional. Information about the affiliate that - received a commission via this transaction + received a commission via this transaction. Can be available only for + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` + and :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + transactions. .. versionadded:: 21.9 - invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload. + invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload. Can be available + only for :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` + transactions. subscription_period (:class:`datetime.timedelta`): Optional. The duration of the paid - subscription + subscription. Can be available only for + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` transactions. .. versionadded:: 21.8 paid_media (tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid - media bought by the user. + media bought by the user. for + :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + transactions only. .. versionadded:: 21.5 - paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload. + paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload. Can be + available only for + :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` transactions. .. versionadded:: 21.6 - gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot + gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot; for + :tg-const:`telegram.constants.TransactionPartnerUser.GIFT_PURCHASE` transactions only. .. versionadded:: 21.8 + premium_subscription_duration (:obj:`int`): Optional. Number of months the gifted Telegram + Premium subscription will be active for; for + :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` + transactions only. + + .. versionadded:: NEXT.VERSION """ @@ -339,7 +391,9 @@ class TransactionPartnerUser(TransactionPartner): "invoice_payload", "paid_media", "paid_media_payload", + "premium_subscription_duration", "subscription_period", + "transaction_type", "user", ) @@ -352,10 +406,15 @@ def __init__( subscription_period: Optional[dtm.timedelta] = None, gift: Optional[Gift] = None, affiliate: Optional[AffiliateInfo] = None, + premium_subscription_duration: Optional[int] = None, + # temporarily optional to account for changed signature + transaction_type: Optional[str] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs) + if transaction_type is None: + raise TypeError("`transaction_type` is a required argument since Bot API 9.0") with self._unfrozen(): self.user: User = user @@ -365,10 +424,13 @@ def __init__( self.paid_media_payload: Optional[str] = paid_media_payload self.subscription_period: Optional[dtm.timedelta] = subscription_period self.gift: Optional[Gift] = gift + self.premium_subscription_duration: Optional[int] = premium_subscription_duration + self.transaction_type: str = transaction_type self._id_attrs = ( self.type, self.user, + self.transaction_type, ) @classmethod diff --git a/telegram/constants.py b/telegram/constants.py index 1d1f2822774..c3c212a19bc 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -114,6 +114,7 @@ "StoryAreaTypeType", "StoryLimit", "TransactionPartnerType", + "TransactionPartnerUser", "UniqueGiftInfoOrigin", "UpdateType", "UserProfilePhotosLimit", @@ -3053,6 +3054,38 @@ class TransactionPartnerType(StringEnum): """:obj:`str`: Transaction with a user.""" +class TransactionPartnerUser(StringEnum): + """This enum contains constants for :class:`telegram.TransactionPartnerUser`. + The enum members of this enumeration are instances of :class:`str` and can be treated as + such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + INVOICE_PAYMENT = "invoice_payment" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + PAID_MEDIA_PAYMENT = "paid_media_payment" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + GIFT_PURCHASE = "gift_purchase" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + PREMIUM_PURCHASE = "premium_purchase" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + BUSINESS_ACCOUNT_TRANSFER = "business_account_transfer" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + + class ParseMode(StringEnum): """This enum contains the available parse modes. The enum members of this enumeration are instances of :class:`str` and can be treated as such. diff --git a/tests/_payment/stars/test_startransactions.py b/tests/_payment/stars/test_startransactions.py index 0878e8cbede..f90361e2b99 100644 --- a/tests/_payment/stars/test_startransactions.py +++ b/tests/_payment/stars/test_startransactions.py @@ -63,6 +63,7 @@ class StarTransactionTestBase: nanostar_amount = 365 date = to_timestamp(dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)) source = TransactionPartnerUser( + transaction_type="premium_purchase", user=User( id=2, is_bot=False, @@ -144,6 +145,7 @@ def test_equality(self): amount=3, date=to_timestamp(dtm.datetime.utcnow()), source=TransactionPartnerUser( + transaction_type="other_type", user=User( id=3, is_bot=False, diff --git a/tests/_payment/stars/test_transactionpartner.py b/tests/_payment/stars/test_transactionpartner.py index 3f795b93ca2..f89568901a6 100644 --- a/tests/_payment/stars/test_transactionpartner.py +++ b/tests/_payment/stars/test_transactionpartner.py @@ -63,6 +63,7 @@ class TransactionPartnerTestBase: first_name="user", last_name="user", ) + transaction_type = "premium_purchase" invoice_payload = "invoice_payload" paid_media = ( PaidMediaVideo( @@ -101,6 +102,7 @@ class TransactionPartnerTestBase: id=3, type=Chat.CHANNEL, ) + premium_subscription_duration = 3 class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase): @@ -137,6 +139,7 @@ def test_subclass(self, offline_bot, tp_type, subclass): "type": tp_type, "commission_per_mille": self.commission_per_mille, "user": self.user.to_dict(), + "transaction_type": self.transaction_type, "request_count": self.request_count, } tp = TransactionPartner.de_json(json_dict, offline_bot) @@ -268,11 +271,13 @@ def test_equality(self, transaction_partner_fragment): @pytest.fixture def transaction_partner_user(): return TransactionPartnerUser( + transaction_type=TransactionPartnerTestBase.transaction_type, user=TransactionPartnerTestBase.user, invoice_payload=TransactionPartnerTestBase.invoice_payload, paid_media=TransactionPartnerTestBase.paid_media, paid_media_payload=TransactionPartnerTestBase.paid_media_payload, subscription_period=TransactionPartnerTestBase.subscription_period, + premium_subscription_duration=TransactionPartnerTestBase.premium_subscription_duration, ) @@ -288,36 +293,48 @@ def test_slot_behaviour(self, transaction_partner_user): def test_de_json(self, offline_bot): json_dict = { "user": self.user.to_dict(), + "transaction_type": self.transaction_type, "invoice_payload": self.invoice_payload, "paid_media": [pm.to_dict() for pm in self.paid_media], "paid_media_payload": self.paid_media_payload, "subscription_period": self.subscription_period.total_seconds(), + "premium_subscription_duration": self.premium_subscription_duration, } tp = TransactionPartnerUser.de_json(json_dict, offline_bot) assert tp.api_kwargs == {} assert tp.type == "user" assert tp.user == self.user + assert tp.transaction_type == self.transaction_type assert tp.invoice_payload == self.invoice_payload assert tp.paid_media == self.paid_media assert tp.paid_media_payload == self.paid_media_payload assert tp.subscription_period == self.subscription_period + assert tp.premium_subscription_duration == self.premium_subscription_duration def test_to_dict(self, transaction_partner_user): json_dict = transaction_partner_user.to_dict() assert json_dict["type"] == self.type + assert json_dict["transaction_type"] == self.transaction_type assert json_dict["user"] == self.user.to_dict() assert json_dict["invoice_payload"] == self.invoice_payload assert json_dict["paid_media"] == [pm.to_dict() for pm in self.paid_media] assert json_dict["paid_media_payload"] == self.paid_media_payload assert json_dict["subscription_period"] == self.subscription_period.total_seconds() + assert json_dict["premium_subscription_duration"] == self.premium_subscription_duration + + def test_transaction_type_is_required_argument(self): + with pytest.raises(TypeError, match="`transaction_type` is a required argument"): + TransactionPartnerUser(user=self.user) def test_equality(self, transaction_partner_user): a = transaction_partner_user b = TransactionPartnerUser( user=self.user, + transaction_type=self.transaction_type, ) c = TransactionPartnerUser( user=User(id=1, is_bot=False, first_name="user", last_name="user"), + transaction_type=self.transaction_type, ) d = User(id=1, is_bot=False, first_name="user", last_name="user") diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 3b086f3464b..6ed80ac5ec0 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -221,6 +221,7 @@ def ptb_ignored_params(object_name: str) -> set[str]: # tags: deprecated NEXT.VERSION, bot api 9.0 "BusinessConnection": {"is_enabled"}, "ChatFullInfo": {"accepted_gift_types"}, + "TransactionPartnerUser": {"transaction_type"}, } From eab006f996182f138e04d75bcf69535ac0363478 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 4 May 2025 14:58:43 +0000 Subject: [PATCH 3/8] Add chango fragment for PR #4781 --- changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml diff --git a/changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml b/changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml new file mode 100644 index 00000000000..8c5db37e42e --- /dev/null +++ b/changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml @@ -0,0 +1,5 @@ +other = "Api 9.0 premium" +[[pull_requests]] +uid = "4781" +author_uid = "aelkheir" +closes_threads = [] From 776b06f16a977b451167f04957f40e09b0d07e9c Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 4 May 2025 18:02:15 +0300 Subject: [PATCH 4/8] Update master chango fragment for PR and delete local one. --- changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml | 4 ++++ changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml diff --git a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml index 23ff35c5224..ebc61003639 100644 --- a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml +++ b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml @@ -36,3 +36,7 @@ closes_threads = [] uid = "4773" author_uid = "aelkheir" closes_threads = [] +[[pull_requests]] +uid = "4781" +author_uid = "aelkheir" +closes_threads = [] diff --git a/changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml b/changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml deleted file mode 100644 index 8c5db37e42e..00000000000 --- a/changes/unreleased/4781.DVbFX4wmztoANY8rbxtWxm.toml +++ /dev/null @@ -1,5 +0,0 @@ -other = "Api 9.0 premium" -[[pull_requests]] -uid = "4781" -author_uid = "aelkheir" -closes_threads = [] From 05a65d7336503262d6cae0c2fedc9acea2e100ea Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Mon, 5 May 2025 12:29:50 +0300 Subject: [PATCH 5/8] Review comments. - Add shortcut User.gift_premium_subscription - Add a missing ".. versionadded" - Update "deprecations" change note to recommend using keyword arguments for added required fields to existing classes. --- .../4756.JT5nmUmGRG6qDEh5ScMn5f.toml | 3 ++ telegram/_payment/stars/transactionpartner.py | 20 +++++++--- telegram/_user.py | 40 +++++++++++++++++++ tests/test_user.py | 30 ++++++++++++++ 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml index ebc61003639..a25d5be650b 100644 --- a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml +++ b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml @@ -6,6 +6,9 @@ This includes the following: - Deprecated the class ``telegram.constants.StarTransactions``. Its only member ``telegram.constants.StarTransactions.NANOSTAR_VALUE`` will be replaced by ``telegram.constants.Nanostar.VALUE``. - Bot API 9.0 deprecated ``BusinessConnection.can_reply`` in favor of ``BusinessConnection.rights`` - Bot API 9.0 deprecated ``ChatFullInfo.can_send_gift`` in favor of ``ChatFullInfo.accepted_gift_types``. +- Bot API 9.0 introduced these new required fields to existing classes: + - ``TransactionPartnerUser.transaction_type`` + Passing these values as positional arguments is deprecated. We encourage you to use keyword arguments instead, as the the signature will be updated in a future release. These deprecations are backward compatible, but we strongly recommend to update your code to use the new members. """ diff --git a/telegram/_payment/stars/transactionpartner.py b/telegram/_payment/stars/transactionpartner.py index 6ac795518fb..cf086f6bff9 100644 --- a/telegram/_payment/stars/transactionpartner.py +++ b/telegram/_payment/stars/transactionpartner.py @@ -299,6 +299,8 @@ class TransactionPartnerUser(TransactionPartner): for Telegram Premium subscriptions gifted by the bot, :tg-const:`telegram.constants.TransactionPartnerUser.BUSINESS_ACCOUNT_TRANSFER` for direct transfers from managed business accounts. + + .. versionadded:: NEXT.VERSION user (:class:`telegram.User`): Information about the user. affiliate (:class:`telegram.AffiliateInfo`, optional): Information about the affiliate that received a commission via this transaction. Can be available only for @@ -340,11 +342,17 @@ class TransactionPartnerUser(TransactionPartner): Attributes: type (:obj:`str`): The type of the transaction partner, always :tg-const:`telegram.TransactionPartner.USER`. - transaction_type (:obj:`str`): Type of the transaction, currently one of “invoice_payment” - for payments via invoices, “paid_media_payment” for payments for paid media, - “gift_purchase” for gifts sent by the bot, “premium_purchase” for Telegram Premium - subscriptions gifted by the bot, “business_account_transfer” for direct transfers from - managed business accounts. + transaction_type (:obj:`str`): Type of the transaction, currently one of + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` for payments via + invoices, :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + for payments for paid media, + :tg-const:`telegram.constants.TransactionPartnerUser.GIFT_PURCHASE` for gifts sent by + the bot, :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` + for Telegram Premium subscriptions gifted by the bot, + :tg-const:`telegram.constants.TransactionPartnerUser.BUSINESS_ACCOUNT_TRANSFER` for + direct transfers from managed business accounts. + + .. versionadded:: NEXT.VERSION user (:class:`telegram.User`): Information about the user. affiliate (:class:`telegram.AffiliateInfo`): Optional. Information about the affiliate that received a commission via this transaction. Can be available only for @@ -413,6 +421,8 @@ def __init__( api_kwargs: Optional[JSONDict] = None, ) -> None: super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs) + + # tags: deprecated NEXT.VERSION, bot api 9.0 if transaction_type is None: raise TypeError("`transaction_type` is a required argument since Bot API 9.0") diff --git a/telegram/_user.py b/telegram/_user.py index 640a3573acc..63cb9625046 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -1698,6 +1698,46 @@ async def send_gift( api_kwargs=api_kwargs, ) + async def gift_premium_subscription( + self, + month_count: int, + star_count: int, + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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.gift_premium_subscription(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.gift_premium_subscription`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().gift_premium_subscription( + user_id=self.id, + month_count=month_count, + star_count=star_count, + text=text, + text_parse_mode=text_parse_mode, + text_entities=text_entities, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def send_copy( self, from_chat_id: Union[str, int], diff --git a/tests/test_user.py b/tests/test_user.py index b7ea5f8bd26..490aa6052ec 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -745,6 +745,36 @@ async def make_assertion(*_, **kwargs): text_entities="text_entities", ) + async def test_instance_method_gift_premium_subscription(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return ( + kwargs["user_id"] == user.id + and kwargs["month_count"] == 3 + and kwargs["star_count"] == 1000 + and kwargs["text"] == "text" + and kwargs["text_parse_mode"] == "text_parse_mode" + and kwargs["text_entities"] == "text_entities" + ) + + assert check_shortcut_signature( + user.gift_premium_subscription, Bot.gift_premium_subscription, ["user_id"], [] + ) + assert await check_shortcut_call( + user.gift_premium_subscription, + user.get_bot(), + "gift_premium_subscription", + ) + assert await check_defaults_handling(user.gift_premium_subscription, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "gift_premium_subscription", make_assertion) + assert await user.gift_premium_subscription( + month_count=3, + star_count=1000, + text="text", + text_parse_mode="text_parse_mode", + text_entities="text_entities", + ) + async def test_instance_method_verify_user(self, monkeypatch, user): async def make_assertion(*_, **kwargs): return ( From 4252e350cd97c9c2aa44e546a3ba1341e89f9abf Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Mon, 5 May 2025 09:34:22 +0000 Subject: [PATCH 6/8] Add chango fragment for PR #4781 --- changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml diff --git a/changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml b/changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml new file mode 100644 index 00000000000..8c5db37e42e --- /dev/null +++ b/changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml @@ -0,0 +1,5 @@ +other = "Api 9.0 premium" +[[pull_requests]] +uid = "4781" +author_uid = "aelkheir" +closes_threads = [] From 64a409dfaa1e1d0f4d47136f20ded4b2a6a39f37 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 12 May 2025 20:35:56 +0200 Subject: [PATCH 7/8] update chango fragment(s) --- changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml | 2 ++ changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml diff --git a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml index c8e90e49a0a..ab772fc1433 100644 --- a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml +++ b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml @@ -8,6 +8,8 @@ This includes the following: - Bot API 9.0 deprecated ``ChatFullInfo.can_send_gift`` in favor of ``ChatFullInfo.accepted_gift_types``. - Bot API 9.0 introduced these new required fields to existing classes: - ``TransactionPartnerUser.transaction_type`` + - ``ChatFullInfo.accepted_gift_types `` + Passing these values as positional arguments is deprecated. We encourage you to use keyword arguments instead, as the the signature will be updated in a future release. These deprecations are backward compatible, but we strongly recommend to update your code to use the new members. diff --git a/changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml b/changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml deleted file mode 100644 index 8c5db37e42e..00000000000 --- a/changes/unreleased/4781.JT5nmUmGRG6qDEh5ScMn5f.toml +++ /dev/null @@ -1,5 +0,0 @@ -other = "Api 9.0 premium" -[[pull_requests]] -uid = "4781" -author_uid = "aelkheir" -closes_threads = [] From 8708fecb9742c9b5bd0ca12c71e7b38a5096a186 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 12 May 2025 21:00:36 +0200 Subject: [PATCH 8/8] doc fix --- changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml index ab772fc1433..a7a6b76c9a7 100644 --- a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml +++ b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml @@ -8,7 +8,7 @@ This includes the following: - Bot API 9.0 deprecated ``ChatFullInfo.can_send_gift`` in favor of ``ChatFullInfo.accepted_gift_types``. - Bot API 9.0 introduced these new required fields to existing classes: - ``TransactionPartnerUser.transaction_type`` - - ``ChatFullInfo.accepted_gift_types `` + - ``ChatFullInfo.accepted_gift_types`` Passing these values as positional arguments is deprecated. We encourage you to use keyword arguments instead, as the the signature will be updated in a future release. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy