From 67a97ae5a7e5225c988abb5d2242b13bfe2ddfbd Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:09:19 -0400 Subject: [PATCH] API 7.10 (#4461, #4460, #4463, #4464) Co-authored-by: aelkheir <90580077+aelkheir@users.noreply.github.com> Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- README.rst | 4 +- docs/source/telegram.at-tree.rst | 1 + docs/source/telegram.ext.handlers-tree.rst | 1 + ...telegram.ext.paidmediapurchasedhandler.rst | 6 + docs/source/telegram.paidmediapurchased.rst | 6 + telegram/__init__.py | 10 +- telegram/_bot.py | 10 +- telegram/_chat.py | 2 + telegram/_chatboost.py | 21 ++- telegram/_giveaway.py | 60 ++++++- telegram/_paidmedia.py | 50 ++++++ telegram/_payment/stars.py | 15 +- telegram/_update.py | 33 +++- telegram/constants.py | 133 ++++++++------ telegram/ext/__init__.py | 2 + telegram/ext/_extbot.py | 2 + .../_handlers/paidmediapurchasedhandler.py | 95 ++++++++++ tests/README.rst | 2 +- tests/ext/test_paidmediapurchasedhandler.py | 169 ++++++++++++++++++ tests/test_chat.py | 5 +- tests/test_chatboost.py | 2 + tests/test_giveaway.py | 46 ++++- tests/test_message.py | 2 +- tests/test_paidmedia.py | 61 +++++++ tests/test_stars.py | 1 + tests/test_update.py | 10 ++ 26 files changed, 668 insertions(+), 81 deletions(-) create mode 100644 docs/source/telegram.ext.paidmediapurchasedhandler.rst create mode 100644 docs/source/telegram.paidmediapurchased.rst create mode 100644 telegram/ext/_handlers/paidmediapurchasedhandler.py create mode 100644 tests/ext/test_paidmediapurchasedhandler.py diff --git a/README.rst b/README.rst index 605942c06..4c7cba543 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-7.9-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.10-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 **7.9** are natively supported by this library. +All types and methods of the Telegram Bot API **7.10** 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/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index bb6e4c814..f40223391 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -120,6 +120,7 @@ Available Types telegram.paidmediainfo telegram.paidmediaphoto telegram.paidmediapreview + telegram.paidmediapurchased telegram.paidmediavideo telegram.photosize telegram.poll diff --git a/docs/source/telegram.ext.handlers-tree.rst b/docs/source/telegram.ext.handlers-tree.rst index 6749cacb9..72e0d824c 100644 --- a/docs/source/telegram.ext.handlers-tree.rst +++ b/docs/source/telegram.ext.handlers-tree.rst @@ -18,6 +18,7 @@ Handlers telegram.ext.inlinequeryhandler telegram.ext.messagehandler telegram.ext.messagereactionhandler + telegram.ext.paidmediapurchasedhandler telegram.ext.pollanswerhandler telegram.ext.pollhandler telegram.ext.precheckoutqueryhandler diff --git a/docs/source/telegram.ext.paidmediapurchasedhandler.rst b/docs/source/telegram.ext.paidmediapurchasedhandler.rst new file mode 100644 index 000000000..19bfbeea3 --- /dev/null +++ b/docs/source/telegram.ext.paidmediapurchasedhandler.rst @@ -0,0 +1,6 @@ +PaidMediaPurchasedHandler +========================= + +.. autoclass:: telegram.ext.PaidMediaPurchasedHandler + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmediapurchased.rst b/docs/source/telegram.paidmediapurchased.rst new file mode 100644 index 000000000..80568ae40 --- /dev/null +++ b/docs/source/telegram.paidmediapurchased.rst @@ -0,0 +1,6 @@ +PaidMediaPurchased +================== + +.. autoclass:: telegram.PaidMediaPurchased + :members: + :show-inheritance: diff --git a/telegram/__init__.py b/telegram/__init__.py index 7b5803a3e..0ff15a7a9 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -180,6 +180,7 @@ __all__ = ( "PaidMediaInfo", "PaidMediaPhoto", "PaidMediaPreview", + "PaidMediaPurchased", "PaidMediaVideo", "PassportData", "PassportElementError", @@ -419,7 +420,14 @@ from ._messageorigin import ( MessageOriginUser, ) from ._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated -from ._paidmedia import PaidMedia, PaidMediaInfo, PaidMediaPhoto, PaidMediaPreview, PaidMediaVideo +from ._paidmedia import ( + PaidMedia, + PaidMediaInfo, + PaidMediaPhoto, + PaidMediaPreview, + PaidMediaPurchased, + PaidMediaVideo, +) from ._passport.credentials import ( Credentials, DataCredentials, diff --git a/telegram/_bot.py b/telegram/_bot.py index b79df08ff..62f25125c 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9193,6 +9193,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. reply_parameters: Optional["ReplyParameters"] = None, reply_markup: Optional[ReplyMarkup] = None, business_connection_id: Optional[str] = None, + payload: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -9211,9 +9212,15 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. Telegram Star proceeds from this media will be credited to the chat's balance. Otherwise, they will be credited to the bot's balance. star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access - to the media. + to the media; :tg-const:`telegram.constants.InvoiceLimit.MIN_STAR_COUNT` - + :tg-const:`telegram.constants.InvoiceLimit.MAX_STAR_COUNT`. media (Sequence[:class:`telegram.InputPaidMedia`]): A list describing the media to be sent; up to :tg-const:`telegram.constants.MediaGroupLimit.MAX_MEDIA_LENGTH` items. + payload (:obj:`str`, optional): Bot-defined paid media payload, + 0-:tg-const:`telegram.constants.InvoiceLimit.MAX_PAYLOAD_LENGTH` bytes. This will + not be displayed to the user, use it for your internal processes. + + .. versionadded:: NEXT.VERSION caption (:obj:`str`, optional): Caption of the media to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. parse_mode (:obj:`str`, optional): |parse_mode| @@ -9252,6 +9259,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. "star_count": star_count, "media": media, "show_caption_above_media": show_caption_above_media, + "payload": payload, } return await self._send_message( diff --git a/telegram/_chat.py b/telegram/_chat.py index 6eb789785..8c5f70524 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -3350,6 +3350,7 @@ class _ChatBase(TelegramObject): reply_parameters: Optional["ReplyParameters"] = None, reply_markup: Optional[ReplyMarkup] = None, business_connection_id: Optional[str] = None, + payload: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3391,6 +3392,7 @@ class _ChatBase(TelegramObject): pool_timeout=pool_timeout, api_kwargs=api_kwargs, business_connection_id=business_connection_id, + payload=payload, ) diff --git a/telegram/_chatboost.py b/telegram/_chatboost.py index 7b972eec6..c39537442 100644 --- a/telegram/_chatboost.py +++ b/telegram/_chatboost.py @@ -187,15 +187,22 @@ class ChatBoostSourceGiftCode(ChatBoostSource): class ChatBoostSourceGiveaway(ChatBoostSource): """ - The boost was obtained by the creation of a Telegram Premium giveaway. This boosts the chat 4 - times for the duration of the corresponding Telegram Premium subscription. + The boost was obtained by the creation of a Telegram Premium giveaway or a Telegram Star. + This boosts the chat 4 times for the duration of the corresponding Telegram Premium + subscription for Telegram Premium giveaways and :attr:`prize_star_count` / 500 times for + one year for Telegram Star giveaways. .. versionadded:: 20.8 Args: giveaway_message_id (:obj:`int`): Identifier of a message in the chat with the giveaway; the message could have been deleted already. May be 0 if the message isn't sent yet. - user (:class:`telegram.User`, optional): User that won the prize in the giveaway if any. + user (:class:`telegram.User`, optional): User that won the prize in the giveaway if any; + for Telegram Premium giveaways only. + prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be split between + giveaway winners; for Telegram Star giveaways only. + + .. versionadded:: NEXT.VERSION is_unclaimed (:obj:`bool`, optional): :obj:`True`, if the giveaway was completed, but there was no user to win the prize. @@ -205,17 +212,22 @@ class ChatBoostSourceGiveaway(ChatBoostSource): giveaway_message_id (:obj:`int`): Identifier of a message in the chat with the giveaway; the message could have been deleted already. May be 0 if the message isn't sent yet. user (:class:`telegram.User`): Optional. User that won the prize in the giveaway if any. + prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be split between + giveaway winners; for Telegram Star giveaways only. + + .. versionadded:: NEXT.VERSION is_unclaimed (:obj:`bool`): Optional. :obj:`True`, if the giveaway was completed, but there was no user to win the prize. """ - __slots__ = ("giveaway_message_id", "is_unclaimed", "user") + __slots__ = ("giveaway_message_id", "is_unclaimed", "prize_star_count", "user") def __init__( self, giveaway_message_id: int, user: Optional[User] = None, is_unclaimed: Optional[bool] = None, + prize_star_count: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -224,6 +236,7 @@ class ChatBoostSourceGiveaway(ChatBoostSource): with self._unfrozen(): self.giveaway_message_id: int = giveaway_message_id self.user: Optional[User] = user + self.prize_star_count: Optional[int] = prize_star_count self.is_unclaimed: Optional[bool] = is_unclaimed diff --git a/telegram/_giveaway.py b/telegram/_giveaway.py index b287433fe..a482f4757 100644 --- a/telegram/_giveaway.py +++ b/telegram/_giveaway.py @@ -56,8 +56,13 @@ class Giveaway(TelegramObject): country codes indicating the countries from which eligible users for the giveaway must come. If empty, then all users can participate in the giveaway. Users with a phone number that was bought on Fragment can always participate in giveaways. + prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be split between + giveaway winners; for Telegram Star giveaways only. + + .. versionadded:: NEXT.VERSION premium_subscription_month_count (:obj:`int`, optional): The number of months the Telegram - Premium subscription won from the giveaway will be active for. + Premium subscription won from the giveaway will be active for; for Telegram Premium + giveaways only. Attributes: chats (Sequence[:class:`telegram.Chat`]): The list of chats which the user must join to @@ -75,8 +80,13 @@ class Giveaway(TelegramObject): country codes indicating the countries from which eligible users for the giveaway must come. If empty, then all users can participate in the giveaway. Users with a phone number that was bought on Fragment can always participate in giveaways. + prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be split between + giveaway winners; for Telegram Star giveaways only. + + .. versionadded:: NEXT.VERSION premium_subscription_month_count (:obj:`int`): Optional. The number of months the Telegram - Premium subscription won from the giveaway will be active for. + Premium subscription won from the giveaway will be active for; for Telegram Premium + giveaways only. """ __slots__ = ( @@ -86,6 +96,7 @@ class Giveaway(TelegramObject): "only_new_members", "premium_subscription_month_count", "prize_description", + "prize_star_count", "winner_count", "winners_selection_date", ) @@ -100,6 +111,7 @@ class Giveaway(TelegramObject): prize_description: Optional[str] = None, country_codes: Optional[Sequence[str]] = None, premium_subscription_month_count: Optional[int] = None, + prize_star_count: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -113,6 +125,7 @@ class Giveaway(TelegramObject): self.prize_description: Optional[str] = prize_description self.country_codes: Tuple[str, ...] = parse_sequence_arg(country_codes) self.premium_subscription_month_count: Optional[int] = premium_subscription_month_count + self.prize_star_count: Optional[int] = prize_star_count self._id_attrs = ( self.chats, @@ -145,13 +158,28 @@ class Giveaway(TelegramObject): class GiveawayCreated(TelegramObject): """This object represents a service message about the creation of a scheduled giveaway. - Currently holds no information. + + Args: + prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be + split between giveaway winners; for Telegram Star giveaways only. + + .. versionadded:: NEXT.VERSION + + Attributes: + prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be + split between giveaway winners; for Telegram Star giveaways only. + + .. versionadded:: NEXT.VERSION + """ - __slots__ = () + __slots__ = ("prize_star_count",) - def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + def __init__( + self, prize_star_count: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None + ): super().__init__(api_kwargs=api_kwargs) + self.prize_star_count: Optional[int] = prize_star_count self._freeze() @@ -173,6 +201,10 @@ class GiveawayWinners(TelegramObject): winner_count (:obj:`int`): Total number of winners in the giveaway winners (Sequence[:class:`telegram.User`]): List of up to :tg-const:`telegram.constants.GiveawayLimit.MAX_WINNERS` winners of the giveaway + prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be split between + giveaway winners; for Telegram Star giveaways only. + + .. versionadded:: NEXT.VERSION additional_chat_count (:obj:`int`, optional): The number of other chats the user had to join in order to be eligible for the giveaway premium_subscription_month_count (:obj:`int`, optional): The number of months the Telegram @@ -194,6 +226,10 @@ class GiveawayWinners(TelegramObject): :tg-const:`telegram.constants.GiveawayLimit.MAX_WINNERS` winners of the giveaway additional_chat_count (:obj:`int`): Optional. The number of other chats the user had to join in order to be eligible for the giveaway + prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be split between + giveaway winners; for Telegram Star giveaways only. + + .. versionadded:: NEXT.VERSION premium_subscription_month_count (:obj:`int`): Optional. The number of months the Telegram Premium subscription won from the giveaway will be active for unclaimed_prize_count (:obj:`int`): Optional. Number of undistributed prizes @@ -211,6 +247,7 @@ class GiveawayWinners(TelegramObject): "only_new_members", "premium_subscription_month_count", "prize_description", + "prize_star_count", "unclaimed_prize_count", "was_refunded", "winner_count", @@ -231,6 +268,7 @@ class GiveawayWinners(TelegramObject): only_new_members: Optional[bool] = None, was_refunded: Optional[bool] = None, prize_description: Optional[str] = None, + prize_star_count: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -247,6 +285,7 @@ class GiveawayWinners(TelegramObject): self.only_new_members: Optional[bool] = only_new_members self.was_refunded: Optional[bool] = was_refunded self.prize_description: Optional[str] = prize_description + self.prize_star_count: Optional[int] = prize_star_count self._id_attrs = ( self.chat, @@ -295,21 +334,29 @@ class GiveawayCompleted(TelegramObject): unclaimed_prize_count (:obj:`int`, optional): Number of undistributed prizes giveaway_message (:class:`telegram.Message`, optional): Message with the giveaway that was completed, if it wasn't deleted + is_star_giveaway (:obj:`bool`, optional): :obj:`True`, if the giveaway is a Telegram Star + giveaway. Otherwise, currently, the giveaway is a Telegram Premium giveaway. + .. versionadded:: NEXT.VERSION Attributes: winner_count (:obj:`int`): Number of winners in the giveaway unclaimed_prize_count (:obj:`int`): Optional. Number of undistributed prizes giveaway_message (:class:`telegram.Message`): Optional. Message with the giveaway that was completed, if it wasn't deleted + is_star_giveaway (:obj:`bool`): Optional. :obj:`True`, if the giveaway is a Telegram Star + giveaway. Otherwise, currently, the giveaway is a Telegram Premium giveaway. + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("giveaway_message", "unclaimed_prize_count", "winner_count") + __slots__ = ("giveaway_message", "is_star_giveaway", "unclaimed_prize_count", "winner_count") def __init__( self, winner_count: int, unclaimed_prize_count: Optional[int] = None, giveaway_message: Optional["Message"] = None, + is_star_giveaway: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -318,6 +365,7 @@ class GiveawayCompleted(TelegramObject): self.winner_count: int = winner_count self.unclaimed_prize_count: Optional[int] = unclaimed_prize_count self.giveaway_message: Optional[Message] = giveaway_message + self.is_star_giveaway: Optional[bool] = is_star_giveaway self._id_attrs = ( self.winner_count, diff --git a/telegram/_paidmedia.py b/telegram/_paidmedia.py index fe78cca28..57126b130 100644 --- a/telegram/_paidmedia.py +++ b/telegram/_paidmedia.py @@ -24,6 +24,7 @@ from telegram import constants from telegram._files.photosize import PhotoSize from telegram._files.video import Video from telegram._telegramobject import TelegramObject +from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict @@ -288,3 +289,52 @@ class PaidMediaInfo(TelegramObject): data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot) return super().de_json(data=data, bot=bot) + + +class PaidMediaPurchased(TelegramObject): + """This object contains information about a paid media purchase. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`from_user` and :attr:`paid_media_payload` are equal. + + Note: + In Python :keyword:`from` is a reserved word. Use :paramref:`from_user` instead. + + .. versionadded:: NEXT.VERSION + + Args: + from_user (:class:`telegram.User`): User who purchased the media. + paid_media_payload (:obj:`str`): Bot-specified paid media payload. + + Attributes: + from_user (:class:`telegram.User`): User who purchased the media. + paid_media_payload (:obj:`str`): Bot-specified paid media payload. + """ + + __slots__ = ("from_user", "paid_media_payload") + + def __init__( + self, + from_user: "User", + paid_media_payload: str, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.from_user: User = from_user + self.paid_media_payload: str = paid_media_payload + + self._id_attrs = (self.from_user, self.paid_media_payload) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaPurchased"]: + data = cls._parse_data(data) + + if not data: + return None + + data["from_user"] = User.de_json(data=data.pop("from"), bot=bot) + return super().de_json(data=data, bot=bot) diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index 94f621d00..2a3f1dca2 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -328,6 +328,9 @@ class TransactionPartnerUser(TransactionPartner): media bought by the user. .. versionadded:: 21.5 + paid_media_payload (:obj:`str`, optional): Optional. Bot-specified paid media payload. + + .. versionadded:: NEXT.VERSION Attributes: type (:obj:`str`): The type of the transaction partner, @@ -338,19 +341,20 @@ class TransactionPartnerUser(TransactionPartner): media bought by the user. .. versionadded:: 21.5 + paid_media_payload (:obj:`str`): Optional. Optional. Bot-specified paid media payload. + + .. versionadded:: NEXT.VERSION + """ - __slots__ = ( - "invoice_payload", - "paid_media", - "user", - ) + __slots__ = ("invoice_payload", "paid_media", "paid_media_payload", "user") def __init__( self, user: "User", invoice_payload: Optional[str] = None, paid_media: Optional[Sequence[PaidMedia]] = None, + paid_media_payload: Optional[str] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: @@ -360,6 +364,7 @@ class TransactionPartnerUser(TransactionPartner): self.user: User = user self.invoice_payload: Optional[str] = invoice_payload self.paid_media: Optional[Tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media) + self.paid_media_payload: Optional[str] = paid_media_payload self._id_attrs = ( self.type, self.user, diff --git a/telegram/_update.py b/telegram/_update.py index 579cb0085..be4f2ec58 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -30,6 +30,7 @@ from telegram._choseninlineresult import ChosenInlineResult from telegram._inline.inlinequery import InlineQuery from telegram._message import Message from telegram._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated +from telegram._paidmedia import PaidMediaPurchased from telegram._payment.precheckoutquery import PreCheckoutQuery from telegram._payment.shippingquery import ShippingQuery from telegram._poll import Poll, PollAnswer @@ -156,6 +157,11 @@ class Update(TelegramObject): .. versionadded:: 21.1 + purchased_paid_media (:class:`telegram.PaidMediaPurchased`, optional): A user purchased + paid media with a non-empty payload sent by the bot in a non-channel chat. + + .. versionadded:: NEXT.VERSION + Attributes: update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a @@ -263,6 +269,11 @@ class Update(TelegramObject): were deleted from a connected business account. .. versionadded:: 21.1 + + purchased_paid_media (:class:`telegram.PaidMediaPurchased`): Optional. A user purchased + paid media with a non-empty payload sent by the bot in a non-channel chat. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -290,6 +301,7 @@ class Update(TelegramObject): "poll", "poll_answer", "pre_checkout_query", + "purchased_paid_media", "removed_chat_boost", "shipping_query", "update_id", @@ -383,6 +395,13 @@ class Update(TelegramObject): """:const:`telegram.constants.UpdateType.DELETED_BUSINESS_MESSAGES` .. versionadded:: 21.1""" + + PURCHASED_PAID_MEDIA: Final[str] = constants.UpdateType.PURCHASED_PAID_MEDIA + """:const:`telegram.constants.UpdateType.PURCHASED_PAID_MEDIA` + + .. versionadded:: NEXT.VERSION + """ + ALL_TYPES: Final[List[str]] = list(constants.UpdateType) """List[:obj:`str`]: A list of all available update types. @@ -413,6 +432,7 @@ class Update(TelegramObject): business_message: Optional[Message] = None, edited_business_message: Optional[Message] = None, deleted_business_messages: Optional[BusinessMessagesDeleted] = None, + purchased_paid_media: Optional[PaidMediaPurchased] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -444,6 +464,7 @@ class Update(TelegramObject): self.deleted_business_messages: Optional[BusinessMessagesDeleted] = ( deleted_business_messages ) + self.purchased_paid_media: Optional[PaidMediaPurchased] = purchased_paid_media self._effective_user: Optional[User] = None self._effective_sender: Optional[Union[User, Chat]] = None @@ -475,6 +496,9 @@ class Update(TelegramObject): This property now also considers :attr:`business_connection`, :attr:`business_message` and :attr:`edited_business_message`. + .. versionchanged:: NEXT.VERSION + This property now also considers :attr:`purchased_paid_media`. + Example: * If :attr:`message` is present, this will give :attr:`telegram.Message.from_user`. @@ -531,6 +555,9 @@ class Update(TelegramObject): elif self.business_connection: user = self.business_connection.user + elif self.purchased_paid_media: + user = self.purchased_paid_media.from_user + self._effective_user = user return user @@ -601,7 +628,8 @@ class Update(TelegramObject): This is the case, if :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, - :attr:`poll_answer`, or :attr:`business_connection` is present. + :attr:`poll_answer`, :attr:`business_connection`, or :attr:`purchased_paid_media` + is present. .. versionchanged:: 21.1 This property now also considers :attr:`business_message`, @@ -768,5 +796,8 @@ class Update(TelegramObject): data["deleted_business_messages"] = BusinessMessagesDeleted.de_json( data.get("deleted_business_messages"), bot ) + data["purchased_paid_media"] = PaidMediaPurchased.de_json( + data.get("purchased_paid_media"), bot + ) return super().de_json(data=data, bot=bot) diff --git a/telegram/constants.py b/telegram/constants.py index 52d69aaca..4d95368ce 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -152,7 +152,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=9) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=10) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -552,6 +552,42 @@ class AccentColor(Enum): """ +class BackgroundTypeType(StringEnum): + """This enum contains the available types of :class:`telegram.BackgroundType`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 21.2 + """ + + __slots__ = () + + FILL = "fill" + """:obj:`str`: A :class:`telegram.BackgroundType` with fill background.""" + WALLPAPER = "wallpaper" + """:obj:`str`: A :class:`telegram.BackgroundType` with wallpaper background.""" + PATTERN = "pattern" + """:obj:`str`: A :class:`telegram.BackgroundType` with pattern background.""" + CHAT_THEME = "chat_theme" + """:obj:`str`: A :class:`telegram.BackgroundType` with chat_theme background.""" + + +class BackgroundFillType(StringEnum): + """This enum contains the available types of :class:`telegram.BackgroundFill`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 21.2 + """ + + __slots__ = () + + SOLID = "solid" + """:obj:`str`: A :class:`telegram.BackgroundFill` with solid fill.""" + GRADIENT = "gradient" + """:obj:`str`: A :class:`telegram.BackgroundFill` with gradient fill.""" + FREEFORM_GRADIENT = "freeform_gradient" + """:obj:`str`: A :class:`telegram.BackgroundFill` with freeform_gradient fill.""" + + class BotCommandLimit(IntEnum): """This enum contains limitations for :class:`telegram.BotCommand` and :meth:`telegram.Bot.set_my_commands`. @@ -833,6 +869,25 @@ class ChatLimit(IntEnum): """ +class ChatSubscriptionLimit(IntEnum): + """This enum contains limitations for + :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_period` and + :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_price`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 21.5 + """ + + __slots__ = () + + SUBSCRIPTION_PERIOD = 2592000 + """:obj:`int`: The number of seconds the subscription will be active.""" + MIN_PRICE = 1 + """:obj:`int`: Amount of stars a user pays, minimum amount the subscription can be set to.""" + MAX_PRICE = 2500 + """:obj:`int`: Amount of stars a user pays, maximum amount the subscription can be set to.""" + + class BackgroundTypeLimit(IntEnum): """This enum contains limitations for :class:`telegram.BackgroundTypeFill`, :class:`telegram.BackgroundTypeWallpaper` and :class:`telegram.BackgroundTypePattern`. @@ -2724,6 +2779,11 @@ class UpdateType(StringEnum): .. versionadded:: 21.1 """ + PURCHASED_PAID_MEDIA = "purchased_paid_media" + """:obj:`str`: Updates with :attr:`telegram.Update.purchased_paid_media`. + + .. versionadded:: NEXT.VERSION + """ class InvoiceLimit(IntEnum): @@ -2795,6 +2855,8 @@ class InvoiceLimit(IntEnum): :meth:`telegram.Bot.send_invoice`. * :paramref:`~telegram.Bot.create_invoice_link.payload` parameter of :meth:`telegram.Bot.create_invoice_link`. + * :paramref:`~telegram.Bot.send_paid_media.payload` parameter of + :meth:`telegram.Bot.send_paid_media`. """ MAX_TIP_AMOUNTS = 4 """:obj:`int`: Maximum length of a :obj:`Sequence` passed as: @@ -2804,6 +2866,20 @@ class InvoiceLimit(IntEnum): * :paramref:`~telegram.Bot.create_invoice_link.suggested_tip_amounts` parameter of :meth:`telegram.Bot.create_invoice_link`. """ + MIN_STAR_COUNT = 1 + """:obj:`int`: Minimum amount of starts that must be paid to buy access to a paid media + passed as :paramref:`~telegram.Bot.send_paid_media.star_count` parameter of + :meth:`telegram.Bot.send_paid_media`. + + .. versionadded:: NEXT.VERSION + """ + MAX_STAR_COUNT = 2500 + """:obj:`int`: Maximum amount of starts that must be paid to buy access to a paid media + passed as :paramref:`~telegram.Bot.send_paid_media.star_count` parameter of + :meth:`telegram.Bot.send_paid_media`. + + .. versionadded:: NEXT.VERSION + """ class UserProfilePhotosLimit(IntEnum): @@ -3066,58 +3142,3 @@ class ReactionEmoji(StringEnum): """:obj:`str`: Woman Shrugging""" POUTING_FACE = "😡" """:obj:`str`: Pouting face""" - - -class BackgroundTypeType(StringEnum): - """This enum contains the available types of :class:`telegram.BackgroundType`. The enum - members of this enumeration are instances of :class:`str` and can be treated as such. - - .. versionadded:: 21.2 - """ - - __slots__ = () - - FILL = "fill" - """:obj:`str`: A :class:`telegram.BackgroundType` with fill background.""" - WALLPAPER = "wallpaper" - """:obj:`str`: A :class:`telegram.BackgroundType` with wallpaper background.""" - PATTERN = "pattern" - """:obj:`str`: A :class:`telegram.BackgroundType` with pattern background.""" - CHAT_THEME = "chat_theme" - """:obj:`str`: A :class:`telegram.BackgroundType` with chat_theme background.""" - - -class BackgroundFillType(StringEnum): - """This enum contains the available types of :class:`telegram.BackgroundFill`. The enum - members of this enumeration are instances of :class:`str` and can be treated as such. - - .. versionadded:: 21.2 - """ - - __slots__ = () - - SOLID = "solid" - """:obj:`str`: A :class:`telegram.BackgroundFill` with solid fill.""" - GRADIENT = "gradient" - """:obj:`str`: A :class:`telegram.BackgroundFill` with gradient fill.""" - FREEFORM_GRADIENT = "freeform_gradient" - """:obj:`str`: A :class:`telegram.BackgroundFill` with freeform_gradient fill.""" - - -class ChatSubscriptionLimit(IntEnum): - """This enum contains limitations for - :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_period` and - :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_price`. - The enum members of this enumeration are instances of :class:`int` and can be treated as such. - - .. versionadded:: 21.5 - """ - - __slots__ = () - - SUBSCRIPTION_PERIOD = 2592000 - """:obj:`int`: The number of seconds the subscription will be active.""" - MIN_PRICE = 1 - """:obj:`int`: Amount of stars a user pays, minimum amount the subscription can be set to.""" - MAX_PRICE = 2500 - """:obj:`int`: Amount of stars a user pays, maximum amount the subscription can be set to.""" diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index 82dbd1c19..432f742ab 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -48,6 +48,7 @@ __all__ = ( "JobQueue", "MessageHandler", "MessageReactionHandler", + "PaidMediaPurchasedHandler", "PersistenceInput", "PicklePersistence", "PollAnswerHandler", @@ -89,6 +90,7 @@ from ._handlers.conversationhandler import ConversationHandler from ._handlers.inlinequeryhandler import InlineQueryHandler from ._handlers.messagehandler import MessageHandler from ._handlers.messagereactionhandler import MessageReactionHandler +from ._handlers.paidmediapurchasedhandler import PaidMediaPurchasedHandler from ._handlers.pollanswerhandler import PollAnswerHandler from ._handlers.pollhandler import PollHandler from ._handlers.precheckoutqueryhandler import PreCheckoutQueryHandler diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 76b17bad0..3d91acf0d 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -4235,6 +4235,7 @@ class ExtBot(Bot, Generic[RLARGS]): reply_parameters: Optional["ReplyParameters"] = None, reply_markup: Optional[ReplyMarkup] = None, business_connection_id: Optional[str] = None, + payload: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -4265,6 +4266,7 @@ class ExtBot(Bot, Generic[RLARGS]): pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), business_connection_id=business_connection_id, + payload=payload, ) async def create_chat_subscription_invite_link( diff --git a/telegram/ext/_handlers/paidmediapurchasedhandler.py b/telegram/ext/_handlers/paidmediapurchasedhandler.py new file mode 100644 index 000000000..13a7cf123 --- /dev/null +++ b/telegram/ext/_handlers/paidmediapurchasedhandler.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the PaidMediaPurchased class.""" + +from typing import Optional + +from telegram import Update +from telegram._utils.defaultvalue import DEFAULT_TRUE +from telegram._utils.types import SCT, DVType +from telegram.ext._handlers.basehandler import BaseHandler +from telegram.ext._utils._update_parsing import parse_chat_id, parse_username +from telegram.ext._utils.types import CCT, RT, HandlerCallback + + +class PaidMediaPurchasedHandler(BaseHandler[Update, CCT, RT]): + """Handler class to handle Telegram + :attr:`purchased paid media `. + + .. versionadded:: NEXT.VERSION + + Args: + callback (:term:`coroutine function`): The callback function for this handler. Will be + called when :meth:`check_update` has determined that an update should be processed by + this handler. Callback signature:: + + async def callback(update: Update, context: CallbackContext) + user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only + those which are from the specified user ID(s). + + username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only + those which are from the specified username(s). + + block (:obj:`bool`, optional): Determines whether the return value of the callback should + be awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. + + .. seealso:: :wiki:`Concurrency` + Attributes: + callback (:term:`coroutine function`): The callback function for this handler. + block (:obj:`bool`): Determines whether the return value of the callback should be + awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. + """ + + __slots__ = ( + "_user_ids", + "_usernames", + ) + + def __init__( + self: "PaidMediaPurchasedHandler[CCT, RT]", + callback: HandlerCallback[Update, CCT, RT], + user_id: Optional[SCT[int]] = None, + username: Optional[SCT[str]] = None, + block: DVType[bool] = DEFAULT_TRUE, + ): + super().__init__(callback, block=block) + + self._user_ids = parse_chat_id(user_id) + self._usernames = parse_username(username) + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handler's :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + if not isinstance(update, Update) or not update.purchased_paid_media: + return False + + if not self._user_ids and not self._usernames: + return True + if update.purchased_paid_media.from_user.id in self._user_ids: + return True + return update.purchased_paid_media.from_user.username in self._usernames diff --git a/tests/README.rst b/tests/README.rst index 69591953b..c9f3cac63 100644 --- a/tests/README.rst +++ b/tests/README.rst @@ -72,7 +72,7 @@ complete and correct. To run it, export an environment variable first: $ export TEST_OFFICIAL=true -and then run ``pytest tests/test_official.py``. Note: You need py 3.10+ to run this test. +and then run ``pytest tests/test_official/test_official.py``. Note: You need py 3.10+ to run this test. We also have another marker, ``@pytest.mark.dev``, which you can add to tests that you want to run selectively. Use as follows: diff --git a/tests/ext/test_paidmediapurchasedhandler.py b/tests/ext/test_paidmediapurchasedhandler.py new file mode 100644 index 000000000..959b9c30c --- /dev/null +++ b/tests/ext/test_paidmediapurchasedhandler.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import asyncio +import datetime + +import pytest + +from telegram import ( + Bot, + CallbackQuery, + Chat, + ChosenInlineResult, + Message, + PaidMediaPurchased, + PreCheckoutQuery, + ShippingQuery, + Update, + User, +) +from telegram._utils.datetime import UTC +from telegram.ext import CallbackContext, JobQueue, PaidMediaPurchasedHandler +from tests.auxil.slots import mro_slots + +message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") + +params = [ + {"message": message}, + {"edited_message": message}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, + {"channel_post": message}, + {"edited_channel_post": message}, + {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, + {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, + {"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, +] + +ids = ( + "message", + "edited_message", + "callback_query", + "channel_post", + "edited_channel_post", + "chosen_inline_result", + "shipping_query", + "pre_checkout_query", + "callback_query_without_message", +) + + +@pytest.fixture(scope="class", params=params, ids=ids) +def false_update(request): + return Update(update_id=2, **request.param) + + +@pytest.fixture(scope="class") +def time(): + return datetime.datetime.now(tz=UTC) + + +@pytest.fixture(scope="class") +def purchased_paid_media(bot): + bc = PaidMediaPurchased( + from_user=User(1, "name", username="user_a", is_bot=False), + paid_media_payload="payload", + ) + bc.set_bot(bot) + return bc + + +@pytest.fixture +def purchased_paid_media_update(bot, purchased_paid_media): + return Update(0, purchased_paid_media=purchased_paid_media) + + +class TestPaidMediaPurchasedHandler: + test_flag = False + + def test_slot_behaviour(self): + action = PaidMediaPurchasedHandler(self.callback) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + @pytest.fixture(autouse=True) + def _reset(self): + self.test_flag = False + + async def callback(self, update, context): + self.test_flag = ( + isinstance(context, CallbackContext) + and isinstance(context.bot, Bot) + and isinstance(update, Update) + and isinstance(context.update_queue, asyncio.Queue) + and isinstance(context.job_queue, JobQueue) + and isinstance(context.user_data, dict) + and isinstance(context.bot_data, dict) + and isinstance( + update.purchased_paid_media, + PaidMediaPurchased, + ) + ) + + def test_with_user_id(self, purchased_paid_media_update): + handler = PaidMediaPurchasedHandler(self.callback, user_id=1) + assert handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, user_id=[1]) + assert handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, user_id=2, username="@user_a") + assert handler.check_update(purchased_paid_media_update) + + handler = PaidMediaPurchasedHandler(self.callback, user_id=2) + assert not handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, user_id=[2]) + assert not handler.check_update(purchased_paid_media_update) + + def test_with_username(self, purchased_paid_media_update): + handler = PaidMediaPurchasedHandler(self.callback, username="user_a") + assert handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, username="@user_a") + assert handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, username=["user_a"]) + assert handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, username=["@user_a"]) + assert handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, user_id=1, username="@user_b") + assert handler.check_update(purchased_paid_media_update) + + handler = PaidMediaPurchasedHandler(self.callback, username="user_b") + assert not handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, username="@user_b") + assert not handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, username=["user_b"]) + assert not handler.check_update(purchased_paid_media_update) + handler = PaidMediaPurchasedHandler(self.callback, username=["@user_b"]) + assert not handler.check_update(purchased_paid_media_update) + + purchased_paid_media_update.purchased_paid_media.from_user._unfreeze() + purchased_paid_media_update.purchased_paid_media.from_user.username = None + assert not handler.check_update(purchased_paid_media_update) + + def test_other_update_types(self, false_update): + handler = PaidMediaPurchasedHandler(self.callback) + assert not handler.check_update(false_update) + assert not handler.check_update(True) + + async def test_context(self, app, purchased_paid_media_update): + handler = PaidMediaPurchasedHandler(callback=self.callback) + app.add_handler(handler) + + async with app: + await app.process_update(purchased_paid_media_update) + assert self.test_flag diff --git a/tests/test_chat.py b/tests/test_chat.py index adf2c42bc..966e82016 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1299,6 +1299,7 @@ class TestChatWithoutRequest(ChatTestBase): and kwargs["media"] == "media" and kwargs["star_count"] == 42 and kwargs["caption"] == "stars" + and kwargs["payload"] == "payload" ) assert check_shortcut_signature(Chat.send_paid_media, Bot.send_paid_media, ["chat_id"], []) @@ -1306,7 +1307,9 @@ class TestChatWithoutRequest(ChatTestBase): assert await check_defaults_handling(chat.send_paid_media, chat.get_bot()) monkeypatch.setattr(chat.get_bot(), "send_paid_media", make_assertion) - assert await chat.send_paid_media(media="media", star_count=42, caption="stars") + assert await chat.send_paid_media( + media="media", star_count=42, caption="stars", payload="payload" + ) def test_mention_html(self): chat = Chat(id=1, type="foo") diff --git a/tests/test_chatboost.py b/tests/test_chatboost.py index 62123df73..e160d8c82 100644 --- a/tests/test_chatboost.py +++ b/tests/test_chatboost.py @@ -50,6 +50,7 @@ class ChatBoostDefaults: user = User(1, "user", False) date = to_timestamp(datetime.datetime.utcnow()) default_source = ChatBoostSourcePremium(user) + prize_star_count = 99 @pytest.fixture(scope="module") @@ -91,6 +92,7 @@ def chat_boost_source_giveaway(): user=ChatBoostDefaults.user, giveaway_message_id=ChatBoostDefaults.giveaway_message_id, is_unclaimed=ChatBoostDefaults.is_unclaimed, + prize_star_count=ChatBoostDefaults.prize_star_count, ) diff --git a/tests/test_giveaway.py b/tests/test_giveaway.py index 4aac4150c..3ef8bb7a1 100644 --- a/tests/test_giveaway.py +++ b/tests/test_giveaway.py @@ -48,6 +48,7 @@ def giveaway(): premium_subscription_month_count=( TestGiveawayWithoutRequest.premium_subscription_month_count ), + prize_star_count=TestGiveawayWithoutRequest.prize_star_count, ) @@ -60,6 +61,7 @@ class TestGiveawayWithoutRequest: prize_description = "prize_description" country_codes = ["DE", "US"] premium_subscription_month_count = 3 + prize_star_count = 99 def test_slot_behaviour(self, giveaway): for attr in giveaway.__slots__: @@ -76,6 +78,7 @@ class TestGiveawayWithoutRequest: "prize_description": self.prize_description, "country_codes": self.country_codes, "premium_subscription_month_count": self.premium_subscription_month_count, + "prize_star_count": self.prize_star_count, } giveaway = Giveaway.de_json(json_dict, offline_bot) @@ -89,6 +92,7 @@ class TestGiveawayWithoutRequest: assert giveaway.prize_description == self.prize_description assert giveaway.country_codes == tuple(self.country_codes) assert giveaway.premium_subscription_month_count == self.premium_subscription_month_count + assert giveaway.prize_star_count == self.prize_star_count assert Giveaway.de_json(None, offline_bot) is None @@ -102,6 +106,7 @@ class TestGiveawayWithoutRequest: "prize_description": self.prize_description, "country_codes": self.country_codes, "premium_subscription_month_count": self.premium_subscription_month_count, + "prize_star_count": self.prize_star_count, } giveaway_raw = Giveaway.de_json(json_dict, raw_bot) @@ -133,6 +138,7 @@ class TestGiveawayWithoutRequest: giveaway_dict["premium_subscription_month_count"] == self.premium_subscription_month_count ) + assert giveaway_dict["prize_star_count"] == self.prize_star_count def test_equality(self, giveaway): a = giveaway @@ -164,15 +170,40 @@ class TestGiveawayWithoutRequest: assert hash(a) != hash(e) +@pytest.fixture(scope="module") +def giveaway_created(): + return GiveawayCreated( + prize_star_count=TestGiveawayCreatedWithoutRequest.prize_star_count, + ) + + class TestGiveawayCreatedWithoutRequest: - def test_slot_behaviour(self): - giveaway_created = GiveawayCreated() + prize_star_count = 99 + + def test_slot_behaviour(self, giveaway_created): for attr in giveaway_created.__slots__: assert getattr(giveaway_created, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(giveaway_created)) == len( set(mro_slots(giveaway_created)) ), "duplicate slot" + def test_de_json(self, bot): + json_dict = { + "prize_star_count": self.prize_star_count, + } + + gac = GiveawayCreated.de_json(json_dict, bot) + assert gac.api_kwargs == {} + assert gac.prize_star_count == self.prize_star_count + + assert Giveaway.de_json(None, bot) is None + + def test_to_dict(self, giveaway_created): + gac_dict = giveaway_created.to_dict() + + assert isinstance(gac_dict, dict) + assert gac_dict["prize_star_count"] == self.prize_star_count + @pytest.fixture(scope="module") def giveaway_winners(): @@ -190,6 +221,7 @@ def giveaway_winners(): additional_chat_count=TestGiveawayWinnersWithoutRequest.additional_chat_count, unclaimed_prize_count=TestGiveawayWinnersWithoutRequest.unclaimed_prize_count, was_refunded=TestGiveawayWinnersWithoutRequest.was_refunded, + prize_star_count=TestGiveawayWinnersWithoutRequest.prize_star_count, ) @@ -205,6 +237,7 @@ class TestGiveawayWinnersWithoutRequest: only_new_members = True was_refunded = True prize_description = "prize_description" + prize_star_count = 99 def test_slot_behaviour(self, giveaway_winners): for attr in giveaway_winners.__slots__: @@ -226,6 +259,7 @@ class TestGiveawayWinnersWithoutRequest: "only_new_members": self.only_new_members, "was_refunded": self.was_refunded, "prize_description": self.prize_description, + "prize_star_count": self.prize_star_count, } giveaway_winners = GiveawayWinners.de_json(json_dict, offline_bot) @@ -245,6 +279,7 @@ class TestGiveawayWinnersWithoutRequest: assert giveaway_winners.only_new_members == self.only_new_members assert giveaway_winners.was_refunded == self.was_refunded assert giveaway_winners.prize_description == self.prize_description + assert giveaway_winners.prize_star_count == self.prize_star_count assert GiveawayWinners.de_json(None, offline_bot) is None @@ -291,6 +326,7 @@ class TestGiveawayWinnersWithoutRequest: assert giveaway_winners_dict["only_new_members"] == self.only_new_members assert giveaway_winners_dict["was_refunded"] == self.was_refunded assert giveaway_winners_dict["prize_description"] == self.prize_description + assert giveaway_winners_dict["prize_star_count"] == self.prize_star_count def test_equality(self, giveaway_winners): a = giveaway_winners @@ -336,12 +372,14 @@ def giveaway_completed(): winner_count=TestGiveawayCompletedWithoutRequest.winner_count, unclaimed_prize_count=TestGiveawayCompletedWithoutRequest.unclaimed_prize_count, giveaway_message=TestGiveawayCompletedWithoutRequest.giveaway_message, + is_star_giveaway=TestGiveawayCompletedWithoutRequest.is_star_giveaway, ) class TestGiveawayCompletedWithoutRequest: winner_count = 42 unclaimed_prize_count = 4 + is_star_giveaway = True giveaway_message = Message( message_id=1, date=dtm.datetime.now(dtm.timezone.utc), @@ -362,6 +400,7 @@ class TestGiveawayCompletedWithoutRequest: "winner_count": self.winner_count, "unclaimed_prize_count": self.unclaimed_prize_count, "giveaway_message": self.giveaway_message.to_dict(), + "is_star_giveaway": self.is_star_giveaway, } giveaway_completed = GiveawayCompleted.de_json(json_dict, offline_bot) @@ -370,6 +409,7 @@ class TestGiveawayCompletedWithoutRequest: assert giveaway_completed.winner_count == self.winner_count assert giveaway_completed.unclaimed_prize_count == self.unclaimed_prize_count assert giveaway_completed.giveaway_message == self.giveaway_message + assert giveaway_completed.is_star_giveaway == self.is_star_giveaway assert GiveawayCompleted.de_json(None, offline_bot) is None @@ -380,6 +420,7 @@ class TestGiveawayCompletedWithoutRequest: assert giveaway_completed_dict["winner_count"] == self.winner_count assert giveaway_completed_dict["unclaimed_prize_count"] == self.unclaimed_prize_count assert giveaway_completed_dict["giveaway_message"] == self.giveaway_message.to_dict() + assert giveaway_completed_dict["is_star_giveaway"] == self.is_star_giveaway def test_equality(self, giveaway_completed): a = giveaway_completed @@ -387,6 +428,7 @@ class TestGiveawayCompletedWithoutRequest: winner_count=self.winner_count, unclaimed_prize_count=self.unclaimed_prize_count, giveaway_message=self.giveaway_message, + is_star_giveaway=self.is_star_giveaway, ) c = GiveawayCompleted( winner_count=self.winner_count + 30, diff --git a/tests/test_message.py b/tests/test_message.py index 02d6e5b5f..602d1e1f8 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -236,7 +236,7 @@ def message(bot): winner_count=5, ) }, - {"giveaway_created": GiveawayCreated()}, + {"giveaway_created": GiveawayCreated(prize_star_count=99)}, { "giveaway_winners": GiveawayWinners( chat=Chat(1, Chat.CHANNEL), diff --git a/tests/test_paidmedia.py b/tests/test_paidmedia.py index 6592125e7..ee2ea9f96 100644 --- a/tests/test_paidmedia.py +++ b/tests/test_paidmedia.py @@ -27,8 +27,10 @@ from telegram import ( PaidMediaInfo, PaidMediaPhoto, PaidMediaPreview, + PaidMediaPurchased, PaidMediaVideo, PhotoSize, + User, Video, ) from telegram.constants import PaidMediaType @@ -122,6 +124,14 @@ def paid_media_info(): ) +@pytest.fixture(scope="module") +def paid_media_purchased(): + return PaidMediaPurchased( + from_user=PaidMediaPurchasedTestBase.from_user, + paid_media_payload=PaidMediaPurchasedTestBase.paid_media_payload, + ) + + class PaidMediaTestBase: width = 640 height = 480 @@ -323,3 +333,54 @@ class TestPaidMediaInfoWithoutRequest(PaidMediaInfoTestBase): assert pmi1 != pmi3 assert hash(pmi1) != hash(pmi3) + + +class PaidMediaPurchasedTestBase: + from_user = User(1, "user", False) + paid_media_payload = "payload" + + +class TestPaidMediaPurchasedWithoutRequest(PaidMediaPurchasedTestBase): + def test_slot_behaviour(self, paid_media_purchased): + inst = paid_media_purchased + 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, bot): + json_dict = { + "from": self.from_user.to_dict(), + "paid_media_payload": self.paid_media_payload, + } + pmp = PaidMediaPurchased.de_json(json_dict, bot) + pmp_none = PaidMediaPurchased.de_json(None, bot) + assert pmp.from_user == self.from_user + assert pmp.paid_media_payload == self.paid_media_payload + assert pmp.api_kwargs == {} + assert pmp_none is None + + def test_to_dict(self, paid_media_purchased): + assert paid_media_purchased.to_dict() == { + "from": self.from_user.to_dict(), + "paid_media_payload": self.paid_media_payload, + } + + def test_equality(self): + pmp1 = PaidMediaPurchased( + from_user=self.from_user, + paid_media_payload=self.paid_media_payload, + ) + pmp2 = PaidMediaPurchased( + from_user=self.from_user, + paid_media_payload=self.paid_media_payload, + ) + pmp3 = PaidMediaPurchased( + from_user=User(2, "user", False), + paid_media_payload="other", + ) + + assert pmp1 == pmp2 + assert hash(pmp1) == hash(pmp2) + + assert pmp1 != pmp3 + assert hash(pmp1) != hash(pmp3) diff --git a/tests/test_stars.py b/tests/test_stars.py index 23ddb4109..d812c7cbb 100644 --- a/tests/test_stars.py +++ b/tests/test_stars.py @@ -74,6 +74,7 @@ def transaction_partner_user(): ] ) ], + paid_media_payload="payload", ) diff --git a/tests/test_update.py b/tests/test_update.py index f2e9dbf61..e9a87c6ca 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -40,6 +40,7 @@ from telegram import ( Message, MessageReactionCountUpdated, MessageReactionUpdated, + PaidMediaPurchased, Poll, PollAnswer, PollOption, @@ -143,6 +144,11 @@ business_message = Message( User(1, "", False), ) +purchased_paid_media = PaidMediaPurchased( + from_user=User(1, "", False), + paid_media_payload="payload", +) + params = [ {"message": message}, @@ -178,6 +184,7 @@ params = [ {"deleted_business_messages": deleted_business_messages}, {"business_message": business_message}, {"edited_business_message": business_message}, + {"purchased_paid_media": purchased_paid_media}, # Must be last to conform with `ids` below! {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, ] @@ -205,6 +212,7 @@ all_types = ( "deleted_business_messages", "business_message", "edited_business_message", + "purchased_paid_media", ) ids = (*all_types, "callback_query_without_message") @@ -290,6 +298,7 @@ class TestUpdateWithoutRequest(UpdateTestBase): or update.poll is not None or update.poll_answer is not None or update.business_connection is not None + or update.purchased_paid_media is not None ): assert chat.id == 1 else: @@ -403,6 +412,7 @@ class TestUpdateWithoutRequest(UpdateTestBase): or update.message_reaction_count is not None or update.deleted_business_messages is not None or update.business_connection is not None + or update.purchased_paid_media is not None ): assert eff_message.message_id == message.message_id else: