diff --git a/README.rst b/README.rst index 1e3570e95be..e8aecc8df93 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.5-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.6-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -79,7 +79,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -All types and methods of the Telegram Bot API **7.5** are supported. +All types and methods of the Telegram Bot API **7.6** are supported. Installing ========== diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index f79f5bd959c..3189de1c1d3 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -33,6 +33,8 @@ - Used for sending media grouped together * - :meth:`~telegram.Bot.send_message` - Used for sending text messages + * - :meth:`~telegram.Bot.send_paid_media` + - Used for sending paid media to channels * - :meth:`~telegram.Bot.send_photo` - Used for sending photos * - :meth:`~telegram.Bot.send_poll` diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 077b124aba4..8d3238a27e4 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -88,6 +88,9 @@ Available Types telegram.inputmediadocument telegram.inputmediaphoto telegram.inputmediavideo + telegram.inputpaidmedia + telegram.inputpaidmediaphoto + telegram.inputpaidmediavideo telegram.inputpolloption telegram.inputsticker telegram.keyboardbutton @@ -113,6 +116,11 @@ Available Types telegram.messageoriginuser telegram.messagereactioncountupdated telegram.messagereactionupdated + telegram.paidmedia + telegram.paidmediainfo + telegram.paidmediaphoto + telegram.paidmediapreview + telegram.paidmediavideo telegram.photosize telegram.poll telegram.pollanswer @@ -125,22 +133,12 @@ Available Types telegram.replykeyboardmarkup telegram.replykeyboardremove telegram.replyparameters - telegram.revenuewithdrawalstate - telegram.revenuewithdrawalstatefailed - telegram.revenuewithdrawalstatepending - telegram.revenuewithdrawalstatesucceeded telegram.sentwebappmessage telegram.shareduser - telegram.startransaction - telegram.startransactions telegram.story telegram.switchinlinequerychosenchat telegram.telegramobject telegram.textquote - telegram.transactionpartner - telegram.transactionpartnerfragment - telegram.transactionpartnerother - telegram.transactionpartneruser telegram.update telegram.user telegram.userchatboosts diff --git a/docs/source/telegram.inputpaidmedia.rst b/docs/source/telegram.inputpaidmedia.rst new file mode 100644 index 00000000000..ecb45d35f6d --- /dev/null +++ b/docs/source/telegram.inputpaidmedia.rst @@ -0,0 +1,6 @@ +InputPaidMedia +============== + +.. autoclass:: telegram.InputPaidMedia + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.inputpaidmediaphoto.rst b/docs/source/telegram.inputpaidmediaphoto.rst new file mode 100644 index 00000000000..f8df55823a2 --- /dev/null +++ b/docs/source/telegram.inputpaidmediaphoto.rst @@ -0,0 +1,6 @@ +InputPaidMediaPhoto +=================== + +.. autoclass:: telegram.InputPaidMediaPhoto + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.inputpaidmediavideo.rst b/docs/source/telegram.inputpaidmediavideo.rst new file mode 100644 index 00000000000..8a3789f5028 --- /dev/null +++ b/docs/source/telegram.inputpaidmediavideo.rst @@ -0,0 +1,6 @@ +InputPaidMediaVideo +=================== + +.. autoclass:: telegram.InputPaidMediaVideo + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.paidmedia.rst b/docs/source/telegram.paidmedia.rst new file mode 100644 index 00000000000..0883310f324 --- /dev/null +++ b/docs/source/telegram.paidmedia.rst @@ -0,0 +1,6 @@ +PaidMedia +========= + +.. autoclass:: telegram.PaidMedia + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmediainfo.rst b/docs/source/telegram.paidmediainfo.rst new file mode 100644 index 00000000000..3c0d1e75c52 --- /dev/null +++ b/docs/source/telegram.paidmediainfo.rst @@ -0,0 +1,6 @@ +PaidMediaInfo +============= + +.. autoclass:: telegram.PaidMediaInfo + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmediaphoto.rst b/docs/source/telegram.paidmediaphoto.rst new file mode 100644 index 00000000000..4092cfcc187 --- /dev/null +++ b/docs/source/telegram.paidmediaphoto.rst @@ -0,0 +1,6 @@ +PaidMediaPhoto +============== + +.. autoclass:: telegram.PaidMediaPhoto + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmediapreview.rst b/docs/source/telegram.paidmediapreview.rst new file mode 100644 index 00000000000..32ff4809d69 --- /dev/null +++ b/docs/source/telegram.paidmediapreview.rst @@ -0,0 +1,6 @@ +PaidMediaPreview +================ + +.. autoclass:: telegram.PaidMediaPreview + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmediavideo.rst b/docs/source/telegram.paidmediavideo.rst new file mode 100644 index 00000000000..30f2377ac86 --- /dev/null +++ b/docs/source/telegram.paidmediavideo.rst @@ -0,0 +1,6 @@ +PaidMediaVideo +============== + +.. autoclass:: telegram.PaidMediaVideo + :members: + :show-inheritance: diff --git a/docs/source/telegram.payments-tree.rst b/docs/source/telegram.payments-tree.rst index 67f686ecc4b..2a09c5415ac 100644 --- a/docs/source/telegram.payments-tree.rst +++ b/docs/source/telegram.payments-tree.rst @@ -8,7 +8,18 @@ Payments telegram.labeledprice telegram.orderinfo telegram.precheckoutquery + telegram.revenuewithdrawalstate + telegram.revenuewithdrawalstatefailed + telegram.revenuewithdrawalstatepending + telegram.revenuewithdrawalstatesucceeded telegram.shippingaddress telegram.shippingoption telegram.shippingquery + telegram.startransaction + telegram.startransactions telegram.successfulpayment + telegram.transactionpartner + telegram.transactionpartnerfragment + telegram.transactionpartnerother + telegram.transactionpartnertelegramads + telegram.transactionpartneruser diff --git a/docs/source/telegram.transactionpartnertelegramads.rst b/docs/source/telegram.transactionpartnertelegramads.rst new file mode 100644 index 00000000000..926b25bdcd4 --- /dev/null +++ b/docs/source/telegram.transactionpartnertelegramads.rst @@ -0,0 +1,7 @@ +TransactionPartnerTelegramAds +============================= + +.. autoclass:: telegram.TransactionPartnerTelegramAds + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/telegram/__init__.py b/telegram/__init__.py index 48ad57298c6..af2336a4ac9 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -142,6 +142,9 @@ "InputMediaPhoto", "InputMediaVideo", "InputMessageContent", + "InputPaidMedia", + "InputPaidMediaPhoto", + "InputPaidMediaVideo", "InputPollOption", "InputSticker", "InputTextMessageContent", @@ -173,6 +176,11 @@ "MessageReactionCountUpdated", "MessageReactionUpdated", "OrderInfo", + "PaidMedia", + "PaidMediaInfo", + "PaidMediaPhoto", + "PaidMediaPreview", + "PaidMediaVideo", "PassportData", "PassportElementError", "PassportElementErrorDataField", @@ -223,6 +231,7 @@ "TransactionPartner", "TransactionPartnerFragment", "TransactionPartnerOther", + "TransactionPartnerTelegramAds", "TransactionPartnerUser", "Update", "User", @@ -333,6 +342,9 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, + InputPaidMedia, + InputPaidMediaPhoto, + InputPaidMediaVideo, ) from ._files.inputsticker import InputSticker from ._files.location import Location @@ -405,6 +417,7 @@ MessageOriginUser, ) from ._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated +from ._paidmedia import PaidMedia, PaidMediaInfo, PaidMediaPhoto, PaidMediaPreview, PaidMediaVideo from ._passport.credentials import ( Credentials, DataCredentials, @@ -436,16 +449,7 @@ from ._payment.shippingaddress import ShippingAddress from ._payment.shippingoption import ShippingOption from ._payment.shippingquery import ShippingQuery -from ._payment.successfulpayment import SuccessfulPayment -from ._poll import InputPollOption, Poll, PollAnswer, PollOption -from ._proximityalerttriggered import ProximityAlertTriggered -from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji -from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote -from ._replykeyboardmarkup import ReplyKeyboardMarkup -from ._replykeyboardremove import ReplyKeyboardRemove -from ._sentwebappmessage import SentWebAppMessage -from ._shared import ChatShared, SharedUser, UsersShared -from ._stars import ( +from ._payment.stars import ( RevenueWithdrawalState, RevenueWithdrawalStateFailed, RevenueWithdrawalStatePending, @@ -455,8 +459,18 @@ TransactionPartner, TransactionPartnerFragment, TransactionPartnerOther, + TransactionPartnerTelegramAds, TransactionPartnerUser, ) +from ._payment.successfulpayment import SuccessfulPayment +from ._poll import InputPollOption, Poll, PollAnswer, PollOption +from ._proximityalerttriggered import ProximityAlertTriggered +from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji +from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote +from ._replykeyboardmarkup import ReplyKeyboardMarkup +from ._replykeyboardremove import ReplyKeyboardRemove +from ._sentwebappmessage import SentWebAppMessage +from ._shared import ChatShared, SharedUser, UsersShared from ._story import Story from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject diff --git a/telegram/_bot.py b/telegram/_bot.py index 81b75419b89..6a1cbfd07af 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -70,7 +70,7 @@ from telegram._files.contact import Contact from telegram._files.document import Document from telegram._files.file import File -from telegram._files.inputmedia import InputMedia +from telegram._files.inputmedia import InputMedia, InputPaidMedia from telegram._files.location import Location from telegram._files.photosize import PhotoSize from telegram._files.sticker import MaskPosition, Sticker, StickerSet @@ -84,11 +84,11 @@ from telegram._menubutton import MenuButton from telegram._message import Message from telegram._messageid import MessageId +from telegram._payment.stars import StarTransactions from telegram._poll import InputPollOption, Poll from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji from telegram._reply import ReplyParameters from telegram._sentwebappmessage import SentWebAppMessage -from telegram._stars import StarTransactions from telegram._telegramobject import TelegramObject from telegram._update import Update from telegram._user import User @@ -578,13 +578,16 @@ def _insert_defaults(self, data: Dict[str, object]) -> None: with new._unfrozen(): new.parse_mode = DefaultValue.get_value(new.parse_mode) data[key] = new - elif key == "media" and isinstance(val, Sequence): + elif ( + key == "media" + and isinstance(val, Sequence) + and not isinstance(val[0], InputPaidMedia) + ): # Copy objects as not to edit them in-place copy_list = [copy.copy(media) for media in val] for media in copy_list: with media._unfrozen(): media.parse_mode = DefaultValue.get_value(media.parse_mode) - data[key] = copy_list # 2) else: @@ -7654,7 +7657,8 @@ async def copy_message( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, ) -> MessageId: - """Use this method to copy messages of any kind. Service messages and invoice messages + """Use this method to copy messages of any kind. Service messages, paid media messages, + giveaway messages, giveaway winners messages, and invoice messages can't be copied. The method is analogous to the method :meth:`forward_message`, but the copied message doesn't have a link to the original message. @@ -7780,11 +7784,12 @@ async def copy_messages( ) -> Tuple["MessageId", ...]: """ Use this method to copy messages of any kind. If some of the specified messages can't be - found or copied, they are skipped. Service messages, giveaway messages, giveaway winners - messages, and invoice messages can't be copied. A quiz poll can be copied only if the value - of the field correct_option_id is known to the bot. The method is analogous to the method - :meth:`forward_messages`, but the copied messages don't have a link to the original - message. Album grouping is kept for copied messages. + found or copied, they are skipped. Service messages, paid media messages, giveaway + messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can + be copied only if the value + of the field :attr:`telegram.Poll.correct_option_id` is known to the bot. The method is + analogous to the method :meth:`forward_messages`, but the copied messages don't have a + link to the original message. Album grouping is kept for copied messages. .. versionadded:: 20.8 @@ -9163,6 +9168,94 @@ async def get_star_transactions( bot=self, ) + async def send_paid_media( + self, + chat_id: Union[str, int], + star_count: int, + media: Sequence["InputPaidMedia"], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional[ReplyMarkup] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = 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, + ) -> Message: + """Use this method to send paid media to channel chats. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access + to the media. + media (Sequence[:class:`telegram.InputPaidMedia`]): A list describing the media to be + sent; up to :tg-const:`telegram.constants.MediaGroupLimit.MAX_MEDIA_LENGTH` items. + 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| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + Returns: + :class:`telegram.Message`: On success, the sent message is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + + data: JSONDict = { + "chat_id": chat_id, + "star_count": star_count, + "media": media, + "show_caption_above_media": show_caption_above_media, + } + + return await self._send_message( + "sendPaidMedia", + data, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + protect_content=protect_content, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -9417,3 +9510,5 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`refund_star_payment`""" getStarTransactions = get_star_transactions """Alias for :meth:`get_star_transactions`""" + sendPaidMedia = send_paid_media + """Alias for :meth:`send_paid_media`""" diff --git a/telegram/_chat.py b/telegram/_chat.py index b5e2d111f1a..2513e0ff334 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -48,6 +48,7 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, + InputPaidMedia, InputPollOption, LabeledPrice, LinkPreviewOptions, @@ -3257,6 +3258,60 @@ async def set_message_reaction( api_kwargs=api_kwargs, ) + async def send_paid_media( + self, + star_count: int, + media: Sequence["InputPaidMedia"], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional[ReplyMarkup] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_paid_media(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.send_paid_media`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + """ + return await self.get_bot().send_paid_media( + chat_id=self.id, + star_count=star_count, + media=media, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + show_caption_above_media=show_caption_above_media, + disable_notification=disable_notification, + protect_content=protect_content, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + class Chat(_ChatBase): """This object represents a chat. diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index fbdc9d6842f..d4bda7d415a 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -195,6 +195,10 @@ class ChatFullInfo(_ChatBase): chats. location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which the supergroup is connected. + can_send_paid_media (:obj:`bool`, optional): :obj:`True`, if paid media messages can be + sent or forwarded to the channel chat. The field is available only for channel chats. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this chat. @@ -345,6 +349,10 @@ class ChatFullInfo(_ChatBase): chats. location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which the supergroup is connected. + can_send_paid_media (:obj:`bool`): Optional. :obj:`True`, if paid media messages can be + sent or forwarded to the channel chat. The field is available only for channel chats. + + .. versionadded:: NEXT.VERSION .. _accent colors: https://core.telegram.org/bots/api#accent-colors .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups @@ -360,6 +368,7 @@ class ChatFullInfo(_ChatBase): "business_intro", "business_location", "business_opening_hours", + "can_send_paid_media", "can_set_sticker_set", "custom_emoji_sticker_set_name", "description", @@ -434,6 +443,7 @@ def __init__( custom_emoji_sticker_set_name: Optional[str] = None, linked_chat_id: Optional[int] = None, location: Optional[ChatLocation] = None, + can_send_paid_media: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -496,6 +506,7 @@ def __init__( self.business_intro: Optional[BusinessIntro] = business_intro self.business_location: Optional[BusinessLocation] = business_location self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours + self.can_send_paid_media: Optional[bool] = can_send_paid_media @classmethod def de_json( diff --git a/telegram/_files/_basethumbedmedium.py b/telegram/_files/_basethumbedmedium.py index ba35ea2e53e..20ff82eab5e 100644 --- a/telegram/_files/_basethumbedmedium.py +++ b/telegram/_files/_basethumbedmedium.py @@ -44,7 +44,7 @@ class _BaseThumbedMedium(_BaseMedium): is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`, optional): File size. - thumbnail (:class:`telegram.PhotoSize`, optional): Thumbnail as defined by sender. + thumbnail (:class:`telegram.PhotoSize`, optional): Thumbnail as defined by the sender. .. versionadded:: 20.2 @@ -54,7 +54,7 @@ class _BaseThumbedMedium(_BaseMedium): is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`): Optional. File size. - thumbnail (:class:`telegram.PhotoSize`): Optional. Thumbnail as defined by sender. + thumbnail (:class:`telegram.PhotoSize`): Optional. Thumbnail as defined by the sender. .. versionadded:: 20.2 diff --git a/telegram/_files/animation.py b/telegram/_files/animation.py index 3459e642778..5191ce83d89 100644 --- a/telegram/_files/animation.py +++ b/telegram/_files/animation.py @@ -39,11 +39,11 @@ class Animation(_BaseThumbedMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - file_name (:obj:`str`, optional): Original animation filename as defined by sender. - mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. + width (:obj:`int`): Video width as defined by the sender. + height (:obj:`int`): Video height as defined by the sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. + file_name (:obj:`str`, optional): Original animation filename as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Animation thumbnail as defined by sender. @@ -56,11 +56,11 @@ class Animation(_BaseThumbedMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - file_name (:obj:`str`): Optional. Original animation filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + width (:obj:`int`): Video width as defined by the sender. + height (:obj:`int`): Video height as defined by the sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. + file_name (:obj:`str`): Optional. Original animation filename as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined by sender. diff --git a/telegram/_files/audio.py b/telegram/_files/audio.py index bf5eb123d00..fb7bc2ce7d1 100644 --- a/telegram/_files/audio.py +++ b/telegram/_files/audio.py @@ -39,12 +39,12 @@ class Audio(_BaseThumbedMedium): or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - duration (:obj:`int`): Duration of the audio in seconds as defined by sender. - performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. - file_name (:obj:`str`, optional): Original filename as defined by sender. - mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. + duration (:obj:`int`): Duration of the audio in seconds as defined by the sender. + performer (:obj:`str`, optional): Performer of the audio as defined by the sender or by + audio tags. + title (:obj:`str`, optional): Title of the audio as defined by the sender or by audio tags. + file_name (:obj:`str`, optional): Original filename as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Thumbnail of the album cover to which the music file belongs. @@ -56,12 +56,12 @@ class Audio(_BaseThumbedMedium): or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - duration (:obj:`int`): Duration of the audio in seconds as defined by sender. - performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. - file_name (:obj:`str`): Optional. Original filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + duration (:obj:`int`): Duration of the audio in seconds as defined by the sender. + performer (:obj:`str`): Optional. Performer of the audio as defined by the sender or by + audio tags. + title (:obj:`str`): Optional. Title of the audio as defined by the sender or by audio tags. + file_name (:obj:`str`): Optional. Original filename as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to which the music file belongs. diff --git a/telegram/_files/document.py b/telegram/_files/document.py index a281ffefeaf..e278dc43e3b 100644 --- a/telegram/_files/document.py +++ b/telegram/_files/document.py @@ -39,10 +39,11 @@ class Document(_BaseThumbedMedium): or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - file_name (:obj:`str`, optional): Original filename as defined by sender. - mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. + file_name (:obj:`str`, optional): Original filename as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. - thumbnail (:class:`telegram.PhotoSize`, optional): Document thumbnail as defined by sender. + thumbnail (:class:`telegram.PhotoSize`, optional): Document thumbnail as defined by the + sender. .. versionadded:: 20.2 @@ -51,10 +52,11 @@ class Document(_BaseThumbedMedium): or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - file_name (:obj:`str`): Optional. Original filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + file_name (:obj:`str`): Optional. Original filename as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. - thumbnail (:class:`telegram.PhotoSize`): Optional. Document thumbnail as defined by sender. + thumbnail (:class:`telegram.PhotoSize`): Optional. Document thumbnail as defined by the + sender. .. versionadded:: 20.2 diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index 0cf5955a4d3..3715682fa3b 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """Base class for Telegram InputMedia Objects.""" -from typing import Optional, Sequence, Tuple, Union +from typing import Final, Optional, Sequence, Tuple, Union from telegram import constants from telegram._files.animation import Animation @@ -115,13 +115,162 @@ def _parse_thumbnail_input(thumbnail: Optional[FileInput]) -> Optional[Union[str ) +class InputPaidMedia(TelegramObject): + """ + Base class for Telegram InputPaidMedia Objects. Currently, it can be one of: + + * :class:`telegram.InputMediaPhoto` + * :class:`telegram.InputMediaVideo` + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of media that the instance represents. + media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.PhotoSize` | :class:`telegram.Video`): File to send. |fileinputnopath| + Lastly you can pass an existing telegram media object of the corresponding type + to send. + + Attributes: + type (:obj:`str`): Type of the input media. + media (:obj:`str` | :class:`telegram.InputFile`): Media to send. + """ + + PHOTO: Final[str] = constants.InputPaidMediaType.PHOTO + """:const:`telegram.constants.InputPaidMediaType.PHOTO`""" + VIDEO: Final[str] = constants.InputPaidMediaType.VIDEO + """:const:`telegram.constants.InputPaidMediaType.VIDEO`""" + + __slots__ = ("media", "type") + + def __init__( + self, + type: str, # pylint: disable=redefined-builtin + media: Union[str, InputFile, PhotoSize, Video], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.InputPaidMediaType, type, type) + self.media: Union[str, InputFile, PhotoSize, Video] = media + + self._freeze() + + +class InputPaidMediaPhoto(InputPaidMedia): + """The paid media to send is a photo. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionadded:: NEXT.VERSION + + Args: + media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.PhotoSize`): File to send. |fileinputnopath| + Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. + + Attributes: + type (:obj:`str`): Type of the media, always + :tg-const:`telegram.constants.InputPaidMediaType.PHOTO`. + media (:obj:`str` | :class:`telegram.InputFile`): Photo to send. + """ + + __slots__ = () + + def __init__( + self, + media: Union[FileInput, PhotoSize], + *, + api_kwargs: Optional[JSONDict] = None, + ): + media = parse_file_input(media, PhotoSize, attach=True, local_mode=True) + super().__init__(type=InputPaidMedia.PHOTO, media=media, api_kwargs=api_kwargs) + self._freeze() + + +class InputPaidMediaVideo(InputPaidMedia): + """ + The paid media to send is a video. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionadded:: NEXT.VERSION + + Note: + * When using a :class:`telegram.Video` for the :attr:`media` attribute, it will take the + width, height and duration from that video, unless otherwise specified with the optional + arguments. + * :paramref:`thumbnail` will be ignored for small video files, for which Telegram can + easily generate thumbnails. However, this behaviour is undocumented and might be + changed by Telegram. + + Args: + media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Video`): File to send. |fileinputnopath| + Lastly you can pass an existing :class:`telegram.Video` object to send. + thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstringnopath| + width (:obj:`int`, optional): Video width. + height (:obj:`int`, optional): Video height. + duration (:obj:`int`, optional): Video duration in seconds. + supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is + suitable for streaming. + + Attributes: + type (:obj:`str`): Type of the media, always + :tg-const:`telegram.constants.InputPaidMediaType.VIDEO`. + media (:obj:`str` | :class:`telegram.InputFile`): Video to send. + thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| + width (:obj:`int`): Optional. Video width. + height (:obj:`int`): Optional. Video height. + duration (:obj:`int`): Optional. Video duration in seconds. + supports_streaming (:obj:`bool`): Optional. :obj:`True`, if the uploaded video is + suitable for streaming. + """ + + __slots__ = ("duration", "height", "supports_streaming", "thumbnail", "width") + + def __init__( + self, + media: Union[FileInput, Video], + thumbnail: Optional[FileInput] = None, + width: Optional[int] = None, + height: Optional[int] = None, + duration: Optional[int] = None, + supports_streaming: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + if isinstance(media, Video): + width = width if width is not None else media.width + height = height if height is not None else media.height + duration = duration if duration is not None else media.duration + media = media.file_id + else: + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + media = parse_file_input(media, attach=True, local_mode=True) + + super().__init__(type=InputPaidMedia.VIDEO, media=media, api_kwargs=api_kwargs) + with self._unfrozen(): + self.thumbnail: Optional[Union[str, InputFile]] = InputMedia._parse_thumbnail_input( + thumbnail + ) + self.width: Optional[int] = width + self.height: Optional[int] = height + self.duration: Optional[int] = duration + self.supports_streaming: Optional[bool] = supports_streaming + + class InputMediaAnimation(InputMedia): """Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. Note: When using a :class:`telegram.Animation` for the :attr:`media` attribute, it will take the - width, height and duration from that video, unless otherwise specified with the optional - arguments. + width, height and duration from that animation, unless otherwise specified with the + optional arguments. .. seealso:: :wiki:`Working with Files and Media ` @@ -510,10 +659,10 @@ class InputMediaAudio(InputMedia): .. versionchanged:: 20.0 |sequenceclassargs| - duration (:obj:`int`, optional): Duration of the audio in seconds as defined by sender. - performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. + duration (:obj:`int`, optional): Duration of the audio in seconds as defined by the sender. + performer (:obj:`str`, optional): Performer of the audio as defined by the sender or by + audio tags. + title (:obj:`str`, optional): Title of the audio as defined by the sender or by audio tags. thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ optional): |thumbdocstringnopath| @@ -533,9 +682,9 @@ class InputMediaAudio(InputMedia): * |tupleclassattrs| * |alwaystuple| duration (:obj:`int`): Optional. Duration of the audio in seconds. - performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. + performer (:obj:`str`): Optional. Performer of the audio as defined by the sender or by + audio tags. + title (:obj:`str`): Optional. Title of the audio as defined by the sender or by audio tags. thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| .. versionadded:: 20.2 diff --git a/telegram/_files/location.py b/telegram/_files/location.py index 45401868720..b2e1458d17f 100644 --- a/telegram/_files/location.py +++ b/telegram/_files/location.py @@ -32,8 +32,8 @@ class Location(TelegramObject): considered equal, if their :attr:`longitude` and :attr:`latitude` are equal. Args: - longitude (:obj:`float`): Longitude as defined by sender. - latitude (:obj:`float`): Latitude as defined by sender. + longitude (:obj:`float`): Longitude as defined by the sender. + latitude (:obj:`float`): Latitude as defined by the sender. horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location, measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`. live_period (:obj:`int`, optional): Time relative to the message sending date, during which @@ -45,8 +45,8 @@ class Location(TelegramObject): approaching another chat member, in meters. For sent live locations only. Attributes: - longitude (:obj:`float`): Longitude as defined by sender. - latitude (:obj:`float`): Latitude as defined by sender. + longitude (:obj:`float`): Longitude as defined by the sender. + latitude (:obj:`float`): Latitude as defined by the sender. horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location, measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`. live_period (:obj:`int`): Optional. Time relative to the message sending date, during which diff --git a/telegram/_files/video.py b/telegram/_files/video.py index d28138c75e1..7a1201c431e 100644 --- a/telegram/_files/video.py +++ b/telegram/_files/video.py @@ -39,11 +39,11 @@ class Video(_BaseThumbedMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - file_name (:obj:`str`, optional): Original filename as defined by sender. - mime_type (:obj:`str`, optional): MIME type of a file as defined by sender. + width (:obj:`int`): Video width as defined by the sender. + height (:obj:`int`): Video height as defined by the sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. + file_name (:obj:`str`, optional): Original filename as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of a file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail. @@ -55,11 +55,11 @@ class Video(_BaseThumbedMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - file_name (:obj:`str`): Optional. Original filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of a file as defined by sender. + width (:obj:`int`): Video width as defined by the sender. + height (:obj:`int`): Video height as defined by the sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. + file_name (:obj:`str`): Optional. Original filename as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of a file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail. diff --git a/telegram/_files/videonote.py b/telegram/_files/videonote.py index 2a1f760c1d5..15b23a69bf2 100644 --- a/telegram/_files/videonote.py +++ b/telegram/_files/videonote.py @@ -42,7 +42,7 @@ class VideoNote(_BaseThumbedMedium): Can't be used to download or reuse the file. length (:obj:`int`): Video width and height (diameter of the video message) as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail. @@ -56,7 +56,7 @@ class VideoNote(_BaseThumbedMedium): Can't be used to download or reuse the file. length (:obj:`int`): Video width and height (diameter of the video message) as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail. diff --git a/telegram/_files/voice.py b/telegram/_files/voice.py index 6c1f4dfb289..ae4fa1d6195 100644 --- a/telegram/_files/voice.py +++ b/telegram/_files/voice.py @@ -35,8 +35,8 @@ class Voice(_BaseMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - duration (:obj:`int`): Duration of the audio in seconds as defined by sender. - mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. + duration (:obj:`int`): Duration of the audio in seconds as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. Attributes: @@ -45,8 +45,8 @@ class Voice(_BaseMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - duration (:obj:`int`): Duration of the audio in seconds as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + duration (:obj:`int`): Duration of the audio in seconds as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. """ diff --git a/telegram/_menubutton.py b/telegram/_menubutton.py index 5856fc8d10e..ef89d8a53eb 100644 --- a/telegram/_menubutton.py +++ b/telegram/_menubutton.py @@ -145,7 +145,10 @@ class MenuButtonWebApp(MenuButton): web_app (:class:`telegram.WebAppInfo`): Description of the Web App that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user using the method :meth:`~telegram.Bot.answerWebAppQuery` - of :class:`~telegram.Bot`. + of :class:`~telegram.Bot`. Alternatively, a ``t.me`` link to a Web App of the bot can + be specified in the object instead of the Web App's URL, in which case the Web App + will be opened as if the user pressed the link. + Attributes: type (:obj:`str`): :tg-const:`telegram.constants.MenuButtonType.WEB_APP`. @@ -153,7 +156,9 @@ class MenuButtonWebApp(MenuButton): web_app (:class:`telegram.WebAppInfo`): Description of the Web App that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user using the method :meth:`~telegram.Bot.answerWebAppQuery` - of :class:`~telegram.Bot`. + of :class:`~telegram.Bot`. Alternatively, a ``t.me`` link to a Web App of the bot can + be specified in the object instead of the Web App's URL, in which case the Web App + will be opened as if the user pressed the link. """ __slots__ = ("text", "web_app") diff --git a/telegram/_message.py b/telegram/_message.py index f5626279a93..b52b2bc9b48 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -52,6 +52,7 @@ from telegram._linkpreviewoptions import LinkPreviewOptions from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged from telegram._messageentity import MessageEntity +from telegram._paidmedia import PaidMediaInfo from telegram._passport.passportdata import PassportData from telegram._payment.invoice import Invoice from telegram._payment.successfulpayment import SuccessfulPayment @@ -380,7 +381,8 @@ class Message(MaybeInaccessibleMessage): .. versionchanged:: 20.0 |sequenceclassargs| - caption (:obj:`str`, optional): Caption for the animation, audio, document, photo, video + caption (:obj:`str`, optional): Caption for the animation, audio, document, paid media, + photo, video or voice, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. contact (:class:`telegram.Contact`, optional): Message is a shared contact, information about the contact. @@ -571,6 +573,10 @@ class Message(MaybeInaccessibleMessage): background set. .. versionadded:: 21.2 + paid_media (:obj:`telegram.PaidMediaInfo`, optional): Message contains paid media; + information about the paid media. + + .. versionadded:: NEXT.VERSION Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -692,7 +698,8 @@ class Message(MaybeInaccessibleMessage): .. versionchanged:: 20.0 |tupleclassattrs| - caption (:obj:`str`): Optional. Caption for the animation, audio, document, photo, video + caption (:obj:`str`): Optional. Caption for the animation, audio, document, paid media, + photo, video or voice, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. contact (:class:`telegram.Contact`): Optional. Message is a shared contact, information about the contact. @@ -884,6 +891,10 @@ class Message(MaybeInaccessibleMessage): background set .. versionadded:: 21.2 + paid_media (:obj:`telegram.PaidMediaInfo`): Optional. Message contains paid media; + information about the paid media. + + .. versionadded:: NEXT.VERSION .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a @@ -950,6 +961,7 @@ class Message(MaybeInaccessibleMessage): "new_chat_members", "new_chat_photo", "new_chat_title", + "paid_media", "passport_data", "photo", "pinned_message", @@ -1067,6 +1079,7 @@ def __init__( chat_background_set: Optional[ChatBackground] = None, effect_id: Optional[str] = None, show_caption_above_media: Optional[bool] = None, + paid_media: Optional[PaidMediaInfo] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1168,6 +1181,7 @@ def __init__( self.chat_background_set: Optional[ChatBackground] = chat_background_set self.effect_id: Optional[str] = effect_id self.show_caption_above_media: Optional[bool] = show_caption_above_media + self.paid_media: Optional[PaidMediaInfo] = paid_media self._effective_attachment = DEFAULT_NONE @@ -1283,6 +1297,7 @@ def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optio data["users_shared"] = UsersShared.de_json(data.get("users_shared"), bot) data["chat_shared"] = ChatShared.de_json(data.get("chat_shared"), bot) data["chat_background_set"] = ChatBackground.de_json(data.get("chat_background_set"), bot) + data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot) # Unfortunately, this needs to be here due to cyclic imports from telegram._giveaway import ( # pylint: disable=import-outside-toplevel @@ -1346,6 +1361,7 @@ def effective_attachment( Location, PassportData, Sequence[PhotoSize], + PaidMediaInfo, Poll, Sticker, Story, @@ -1369,6 +1385,7 @@ def effective_attachment( * :class:`telegram.Location` * :class:`telegram.PassportData` * List[:class:`telegram.PhotoSize`] + * :class:`telegram.PaidMediaInfo` * :class:`telegram.Poll` * :class:`telegram.Sticker` * :class:`telegram.Story` @@ -1386,6 +1403,9 @@ def effective_attachment( :attr:`dice`, :attr:`passport_data` and :attr:`poll` are now also considered to be an attachment. + .. versionchanged:: NEXT.VERSION + :attr:`paid_media` is now also considered to be an attachment. + """ if not isinstance(self._effective_attachment, DefaultValue): return self._effective_attachment diff --git a/telegram/_paidmedia.py b/telegram/_paidmedia.py new file mode 100644 index 00000000000..82594ada337 --- /dev/null +++ b/telegram/_paidmedia.py @@ -0,0 +1,290 @@ +#!/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 objects that represent paid media in Telegram.""" + +from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type + +from telegram import constants +from telegram._files.photosize import PhotoSize +from telegram._files.video import Video +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class PaidMedia(TelegramObject): + """Describes the paid media added to a message. Currently, it can be one of: + + * :class:`telegram.PaidMediaPreview` + * :class:`telegram.PaidMediaPhoto` + * :class:`telegram.PaidMediaVideo` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the paid media. + + Attributes: + type (:obj:`str`): Type of the paid media. + """ + + __slots__ = ("type",) + + PREVIEW: Final[str] = constants.PaidMediaType.PREVIEW + """:const:`telegram.constants.PaidMediaType.PREVIEW`""" + PHOTO: Final[str] = constants.PaidMediaType.PHOTO + """:const:`telegram.constants.PaidMediaType.PHOTO`""" + VIDEO: Final[str] = constants.PaidMediaType.VIDEO + """:const:`telegram.constants.PaidMediaType.VIDEO`""" + + def __init__( + self, + type: str, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.PaidMediaType, type, type) + + self._id_attrs = (self.type,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMedia"]: + """Converts JSON data to the appropriate :class:`PaidMedia` object, i.e. takes + care of selecting the correct subclass. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`, optional): The bot associated with this object. + + Returns: + The Telegram object. + + """ + data = cls._parse_data(data) + + if data is None: + return None + + if not data and cls is PaidMedia: + return None + + _class_mapping: Dict[str, Type[PaidMedia]] = { + cls.PREVIEW: PaidMediaPreview, + cls.PHOTO: PaidMediaPhoto, + cls.VIDEO: PaidMediaVideo, + } + + if cls is PaidMedia and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) + + return super().de_json(data=data, bot=bot) + + +class PaidMediaPreview(PaidMedia): + """The paid media isn't available before the payment. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`width`, :attr:`height`, and :attr:`duration` + are equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PREVIEW`. + width (:obj:`int`, optional): Media width as defined by the sender. + height (:obj:`int`, optional): Media height as defined by the sender. + duration (:obj:`int`, optional): Duration of the media in seconds as defined by the sender. + + Attributes: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PREVIEW`. + width (:obj:`int`): Optional. Media width as defined by the sender. + height (:obj:`int`): Optional. Media height as defined by the sender. + duration (:obj:`int`): Optional. Duration of the media in seconds as defined by the sender. + """ + + __slots__ = ("duration", "height", "width") + + def __init__( + self, + width: Optional[int] = None, + height: Optional[int] = None, + duration: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=PaidMedia.PREVIEW, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.width: Optional[int] = width + self.height: Optional[int] = height + self.duration: Optional[int] = duration + + self._id_attrs = (self.type, self.width, self.height, self.duration) + + +class PaidMediaPhoto(PaidMedia): + """ + The paid media is a photo. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`photo` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PHOTO`. + photo (Sequence[:class:`telegram.PhotoSize`]): The photo. + + Attributes: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PHOTO`. + photo (Tuple[:class:`telegram.PhotoSize`]): The photo. + """ + + __slots__ = ("photo",) + + def __init__( + self, + photo: Sequence["PhotoSize"], + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=PaidMedia.PHOTO, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.photo: Tuple[PhotoSize, ...] = parse_sequence_arg(photo) + + self._id_attrs = (self.type, self.photo) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaPhoto"]: + data = cls._parse_data(data) + + if not data: + return None + + data["photo"] = PhotoSize.de_list(data.get("photo"), bot=bot) + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class PaidMediaVideo(PaidMedia): + """ + The paid media is a video. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`video` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.VIDEO`. + video (:class:`telegram.Video`): The video. + + Attributes: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.VIDEO`. + video (:class:`telegram.Video`): The video. + """ + + __slots__ = ("video",) + + def __init__( + self, + video: Video, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=PaidMedia.VIDEO, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.video: Video = video + + self._id_attrs = (self.type, self.video) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaVideo"]: + data = cls._parse_data(data) + + if not data: + return None + + data["video"] = Video.de_json(data.get("video"), bot=bot) + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class PaidMediaInfo(TelegramObject): + """ + Describes the paid media added to a message. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`star_count` and :attr:`paid_media` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access to + the media. + paid_media (Sequence[:class:`telegram.PaidMedia`]): Information about the paid media. + + Attributes: + star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access to + the media. + paid_media (Tuple[:class:`telegram.PaidMedia`]): Information about the paid media. + """ + + __slots__ = ("paid_media", "star_count") + + def __init__( + self, + star_count: int, + paid_media: Sequence[PaidMedia], + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.star_count: int = star_count + self.paid_media: Tuple[PaidMedia, ...] = parse_sequence_arg(paid_media) + + self._id_attrs = (self.star_count, self.paid_media) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaInfo"]: + data = cls._parse_data(data) + + if not data: + return None + + data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot) + return super().de_json(data=data, bot=bot) diff --git a/telegram/_payment/precheckoutquery.py b/telegram/_payment/precheckoutquery.py index 1e7dca7bf33..30ae30be797 100644 --- a/telegram/_payment/precheckoutquery.py +++ b/telegram/_payment/precheckoutquery.py @@ -50,7 +50,7 @@ class PreCheckoutQuery(TelegramObject): `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user. @@ -66,7 +66,7 @@ class PreCheckoutQuery(TelegramObject): `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. diff --git a/telegram/_payment/shippingquery.py b/telegram/_payment/shippingquery.py index 47a62192489..cf81b4ecfa6 100644 --- a/telegram/_payment/shippingquery.py +++ b/telegram/_payment/shippingquery.py @@ -43,13 +43,13 @@ class ShippingQuery(TelegramObject): Args: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address. Attributes: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address. diff --git a/telegram/_stars.py b/telegram/_payment/stars.py similarity index 93% rename from telegram/_stars.py rename to telegram/_payment/stars.py index 8cb6ac1311f..6c537532c37 100644 --- a/telegram/_stars.py +++ b/telegram/_payment/stars.py @@ -183,8 +183,9 @@ class TransactionPartner(TelegramObject): """This object describes the source of a transaction, or its recipient for outgoing transactions. Currently, it can be one of: - * :class:`TransactionPartnerFragment` * :class:`TransactionPartnerUser` + * :class:`TransactionPartnerFragment` + * :class:`TransactionPartnerTelegramAds` * :class:`TransactionPartnerOther` Objects of this class are comparable in terms of equality. Two objects of this class are @@ -207,6 +208,8 @@ class TransactionPartner(TelegramObject): """:const:`telegram.constants.TransactionPartnerType.USER`""" OTHER: Final[str] = constants.TransactionPartnerType.OTHER """:const:`telegram.constants.TransactionPartnerType.OTHER`""" + TELEGRAM_ADS: Final[str] = constants.TransactionPartnerType.TELEGRAM_ADS + """:const:`telegram.constants.TransactionPartnerType.TELEGRAM_ADS`""" def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None: super().__init__(api_kwargs=api_kwargs) @@ -242,6 +245,7 @@ def de_json( cls.FRAGMENT: TransactionPartnerFragment, cls.USER: TransactionPartnerUser, cls.OTHER: TransactionPartnerOther, + cls.TELEGRAM_ADS: TransactionPartnerTelegramAds, } if cls is TransactionPartner and data.get("type") in _class_mapping: @@ -305,20 +309,29 @@ class TransactionPartnerUser(TransactionPartner): Args: user (:class:`telegram.User`): Information about the user. + invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. Attributes: type (:obj:`str`): The type of the transaction partner, always :tg-const:`telegram.TransactionPartner.USER`. user (:class:`telegram.User`): Information about the user. + invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload. """ - __slots__ = ("user",) + __slots__ = ("invoice_payload", "user") - def __init__(self, user: "User", *, api_kwargs: Optional[JSONDict] = None) -> None: + def __init__( + self, + user: "User", + invoice_payload: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs) with self._unfrozen(): self.user: User = user + self.invoice_payload: Optional[str] = invoice_payload self._id_attrs = ( self.type, self.user, @@ -355,6 +368,23 @@ def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: self._freeze() +class TransactionPartnerTelegramAds(TransactionPartner): + """Describes a withdrawal transaction to the Telegram Ads platform. + + .. versionadded:: NEXT.VERSION + + Attributes: + type (:obj:`str`): The type of the transaction partner, + always :tg-const:`telegram.TransactionPartner.TELEGRAM_ADS`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(type=TransactionPartner.TELEGRAM_ADS, api_kwargs=api_kwargs) + self._freeze() + + class StarTransaction(TelegramObject): """Describes a Telegram Star transaction. diff --git a/telegram/_payment/successfulpayment.py b/telegram/_payment/successfulpayment.py index 5298f66801e..90b1545b2d5 100644 --- a/telegram/_payment/successfulpayment.py +++ b/telegram/_payment/successfulpayment.py @@ -44,7 +44,7 @@ class SuccessfulPayment(TelegramObject): `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user. @@ -60,7 +60,7 @@ class SuccessfulPayment(TelegramObject): `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. diff --git a/telegram/_poll.py b/telegram/_poll.py index 01ec75ca5fa..8ea387a0950 100644 --- a/telegram/_poll.py +++ b/telegram/_poll.py @@ -38,7 +38,7 @@ class InputPollOption(TelegramObject): """ - This object contains information about one answer option in a poll to send. + This object contains information about one answer option in a poll to be sent. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`text` is equal. diff --git a/telegram/_reply.py b/telegram/_reply.py index 3ca342d067b..6c04847de64 100644 --- a/telegram/_reply.py +++ b/telegram/_reply.py @@ -37,6 +37,7 @@ from telegram._linkpreviewoptions import LinkPreviewOptions from telegram._messageentity import MessageEntity from telegram._messageorigin import MessageOrigin +from telegram._paidmedia import PaidMediaInfo from telegram._payment.invoice import Invoice from telegram._poll import Poll from telegram._story import Story @@ -101,6 +102,10 @@ class ExternalReplyInfo(TelegramObject): poll (:class:`telegram.Poll`, optional): Message is a native poll, information about the poll. venue (:class:`telegram.Venue`, optional): Message is a venue, information about the venue. + paid_media (:class:`telegram.PaidMedia`, optional): Message contains paid media; + information about the paid media. + + .. versionadded:: NEXT.VERSION Attributes: origin (:class:`telegram.MessageOrigin`): Origin of the message replied to by the given @@ -144,6 +149,10 @@ class ExternalReplyInfo(TelegramObject): poll (:class:`telegram.Poll`): Optional. Message is a native poll, information about the poll. venue (:class:`telegram.Venue`): Optional. Message is a venue, information about the venue. + paid_media (:class:`telegram.PaidMedia`): Optional. Message contains paid media; + information about the paid media. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -162,6 +171,7 @@ class ExternalReplyInfo(TelegramObject): "location", "message_id", "origin", + "paid_media", "photo", "poll", "sticker", @@ -197,6 +207,7 @@ def __init__( location: Optional[Location] = None, poll: Optional[Poll] = None, venue: Optional[Venue] = None, + paid_media: Optional[PaidMediaInfo] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -225,6 +236,7 @@ def __init__( self.location: Optional[Location] = location self.poll: Optional[Poll] = poll self.venue: Optional[Venue] = venue + self.paid_media: Optional[PaidMediaInfo] = paid_media self._id_attrs = (self.origin,) @@ -263,6 +275,7 @@ def de_json( data["location"] = Location.de_json(data.get("location"), bot) data["poll"] = Poll.de_json(data.get("poll"), bot) data["venue"] = Venue.de_json(data.get("venue"), bot) + data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot) return super().de_json(data=data, bot=bot) diff --git a/telegram/constants.py b/telegram/constants.py index 5cd6e7ffc2c..d9b26b417c6 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -71,6 +71,7 @@ "InlineQueryResultType", "InlineQueryResultsButtonLimit", "InputMediaType", + "InputPaidMediaType", "InvoiceLimit", "KeyboardButtonRequestUsersLimit", "LocationLimit", @@ -82,6 +83,7 @@ "MessageLimit", "MessageOriginType", "MessageType", + "PaidMediaType", "ParseMode", "PollLimit", "PollType", @@ -149,7 +151,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=5) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=6) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -1259,6 +1261,21 @@ class InputMediaType(StringEnum): """:obj:`str`: Type of :class:`telegram.InputMediaVideo`.""" +class InputPaidMediaType(StringEnum): + """This enum contains the available types of :class:`telegram.InputPaidMedia`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + PHOTO = "photo" + """:obj:`str`: Type of :class:`telegram.InputMediaPhoto`.""" + VIDEO = "video" + """:obj:`str`: Type of :class:`telegram.InputMediaVideo`.""" + + class InlineQueryLimit(IntEnum): """This enum contains limitations for :class:`telegram.InlineQuery`/ :meth:`telegram.Bot.answer_inline_query`. The enum members of this enumeration are instances @@ -1602,6 +1619,11 @@ class MessageAttachmentType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.invoice`.""" LOCATION = "location" """:obj:`str`: Messages with :attr:`telegram.Message.location`.""" + PAID_MEDIA = "paid_media" + """:obj:`str`: Messages with :attr:`telegram.Message.paid_media`. + + .. versionadded:: NEXT.VERSION + """ PASSPORT_DATA = "passport_data" """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" PHOTO = "photo" @@ -1883,6 +1905,11 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.new_chat_title`.""" NEW_CHAT_PHOTO = "new_chat_photo" """:obj:`str`: Messages with :attr:`telegram.Message.new_chat_photo`.""" + PAID_MEDIA = "paid_media" + """:obj:`str`: Messages with :attr:`telegram.Message.paid_media`. + + .. versionadded:: NEXT.VERSION + """ PASSPORT_DATA = "passport_data" """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" PHOTO = "photo" @@ -1951,6 +1978,24 @@ class MessageType(StringEnum): """ +class PaidMediaType(StringEnum): + """ + This enum contains the available types of :class:`telegram.PaidMedia`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + PREVIEW = "preview" + """:obj:`str`: The type of :class:`telegram.PaidMediaPreview`.""" + VIDEO = "video" + """:obj:`str`: The type of :class:`telegram.PaidMediaVideo`.""" + PHOTO = "photo" + """:obj:`str`: The type of :class:`telegram.PaidMediaPhoto`.""" + + class PollingLimit(IntEnum): """This enum contains limitations for :paramref:`telegram.Bot.get_updates.limit`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. @@ -2490,6 +2535,8 @@ class TransactionPartnerType(StringEnum): """:obj:`str`: Transaction with a user.""" OTHER = "other" """:obj:`str`: Transaction with unknown source or recipient.""" + TELEGRAM_ADS = "telegram_ads" + """:obj:`str`: Transaction with Telegram Ads.""" class ParseMode(StringEnum): diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 3cd4ab389e7..7d8d10e4902 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -107,6 +107,7 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, + InputPaidMedia, InputSticker, LabeledPrice, MessageEntity, @@ -4216,6 +4217,50 @@ async def get_star_transactions( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def send_paid_media( + self, + chat_id: Union[str, int], + star_count: int, + media: Sequence["InputPaidMedia"], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional[ReplyMarkup] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = 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, + ) -> Message: + return await super().send_paid_media( + chat_id=chat_id, + star_count=star_count, + media=media, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + show_caption_above_media=show_caption_above_media, + disable_notification=disable_notification, + protect_content=protect_content, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + # updated camelCase aliases getMe = get_me sendMessage = send_message @@ -4339,3 +4384,4 @@ async def get_star_transactions( replaceStickerInSet = replace_sticker_in_set refundStarPayment = refund_star_payment getStarTransactions = get_star_transactions + sendPaidMedia = send_paid_media diff --git a/telegram/request/_requestparameter.py b/telegram/request/_requestparameter.py index 6b16a5cae66..2e14a8be6ba 100644 --- a/telegram/request/_requestparameter.py +++ b/telegram/request/_requestparameter.py @@ -23,7 +23,7 @@ from typing import List, Optional, Sequence, Tuple, final from telegram._files.inputfile import InputFile -from telegram._files.inputmedia import InputMedia +from telegram._files.inputmedia import InputMedia, InputPaidMedia from telegram._files.inputsticker import InputSticker from telegram._telegramobject import TelegramObject from telegram._utils.datetime import to_timestamp @@ -117,7 +117,7 @@ def _value_and_input_files_from_input( # pylint: disable=too-many-return-statem return value.attach_uri, [value] return None, [value] - if isinstance(value, InputMedia) and isinstance(value.media, InputFile): + if isinstance(value, (InputMedia, InputPaidMedia)) and isinstance(value.media, InputFile): # We call to_dict and change the returned dict instead of overriding # value.media in case the same value is reused for another request data = value.to_dict() diff --git a/tests/_files/test_inputmedia.py b/tests/_files/test_inputmedia.py index cce7fdc07a5..d25a679ffc4 100644 --- a/tests/_files/test_inputmedia.py +++ b/tests/_files/test_inputmedia.py @@ -31,6 +31,8 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, + InputPaidMediaPhoto, + InputPaidMediaVideo, Message, MessageEntity, ReplyParameters, @@ -134,6 +136,25 @@ def input_media_document(class_thumb_file): ) +@pytest.fixture(scope="module") +def input_paid_media_photo(): + return InputPaidMediaPhoto( + media=TestInputMediaPhotoBase.media, + ) + + +@pytest.fixture(scope="module") +def input_paid_media_video(class_thumb_file): + return InputPaidMediaVideo( + media=TestInputMediaVideoBase.media, + thumbnail=class_thumb_file, + width=TestInputMediaVideoBase.width, + height=TestInputMediaVideoBase.height, + duration=TestInputMediaVideoBase.duration, + supports_streaming=TestInputMediaVideoBase.supports_streaming, + ) + + class TestInputMediaVideoBase: type_ = "video" media = "NOTAREALFILEID" @@ -514,6 +535,91 @@ def test_with_local_files(self): assert input_media_document.thumbnail == data_file("telegram.jpg").as_uri() +class TestInputPaidMediaPhotoWithoutRequest(TestInputMediaPhotoBase): + def test_slot_behaviour(self, input_paid_media_photo): + inst = input_paid_media_photo + 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_expected_values(self, input_paid_media_photo): + assert input_paid_media_photo.type == self.type_ + assert input_paid_media_photo.media == self.media + + def test_to_dict(self, input_paid_media_photo): + input_paid_media_photo_dict = input_paid_media_photo.to_dict() + assert input_paid_media_photo_dict["type"] == input_paid_media_photo.type + assert input_paid_media_photo_dict["media"] == input_paid_media_photo.media + + def test_with_photo(self, photo): # noqa: F811 + # fixture found in test_photo + input_paid_media_photo = InputPaidMediaPhoto(photo) + assert input_paid_media_photo.type == self.type_ + assert input_paid_media_photo.media == photo.file_id + + def test_with_photo_file(self, photo_file): # noqa: F811 + # fixture found in test_photo + input_paid_media_photo = InputPaidMediaPhoto(photo_file) + assert input_paid_media_photo.type == self.type_ + assert isinstance(input_paid_media_photo.media, InputFile) + + def test_with_local_files(self): + input_paid_media_photo = InputPaidMediaPhoto(data_file("telegram.jpg")) + assert input_paid_media_photo.media == data_file("telegram.jpg").as_uri() + + +class TestInputPaidMediaVideoWithoutRequest(TestInputMediaVideoBase): + def test_slot_behaviour(self, input_paid_media_video): + inst = input_paid_media_video + 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_expected_values(self, input_paid_media_video): + assert input_paid_media_video.type == self.type_ + assert input_paid_media_video.media == self.media + assert input_paid_media_video.width == self.width + assert input_paid_media_video.height == self.height + assert input_paid_media_video.duration == self.duration + assert input_paid_media_video.supports_streaming == self.supports_streaming + assert isinstance(input_paid_media_video.thumbnail, InputFile) + + def test_to_dict(self, input_paid_media_video): + input_paid_media_video_dict = input_paid_media_video.to_dict() + assert input_paid_media_video_dict["type"] == input_paid_media_video.type + assert input_paid_media_video_dict["media"] == input_paid_media_video.media + assert input_paid_media_video_dict["width"] == input_paid_media_video.width + assert input_paid_media_video_dict["height"] == input_paid_media_video.height + assert input_paid_media_video_dict["duration"] == input_paid_media_video.duration + assert ( + input_paid_media_video_dict["supports_streaming"] + == input_paid_media_video.supports_streaming + ) + assert input_paid_media_video_dict["thumbnail"] == input_paid_media_video.thumbnail + + def test_with_video(self, video): # noqa: F811 + # fixture found in test_video + input_paid_media_video = InputPaidMediaVideo(video) + assert input_paid_media_video.type == self.type_ + assert input_paid_media_video.media == video.file_id + assert input_paid_media_video.width == video.width + assert input_paid_media_video.height == video.height + assert input_paid_media_video.duration == video.duration + + def test_with_video_file(self, video_file): # noqa: F811 + # fixture found in test_video + input_paid_media_video = InputPaidMediaVideo(video_file) + assert input_paid_media_video.type == self.type_ + assert isinstance(input_paid_media_video.media, InputFile) + + def test_with_local_files(self): + input_paid_media_video = InputPaidMediaVideo( + data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg") + ) + assert input_paid_media_video.media == data_file("telegram.mp4").as_uri() + assert input_paid_media_video.thumbnail == data_file("telegram.jpg").as_uri() + + @pytest.fixture(scope="module") def media_group(photo, thumb): # noqa: F811 return [ @@ -1044,3 +1150,20 @@ def build_media(parse_mode, med_type): assert message.caption_entities == () # make sure that the media was not modified assert media.parse_mode == copied_media.parse_mode + + async def test_send_paid_media(self, bot, channel_id, photo_file, video_file): # noqa: F811 + msg = await bot.send_paid_media( + chat_id=channel_id, + star_count=20, + media=[ + InputPaidMediaPhoto(media=photo_file), + InputPaidMediaVideo(media=video_file), + ], + caption="bye onlyfans", + show_caption_above_media=True, + ) + + assert isinstance(msg, Message) + assert msg.caption == "bye onlyfans" + assert msg.show_caption_above_media + assert msg.paid_media.star_count == 20 diff --git a/tests/test_chat.py b/tests/test_chat.py index a11b40c647b..682bdbe514a 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1244,6 +1244,22 @@ async def make_assertion(*_, **kwargs): 123, [ReactionTypeEmoji(ReactionEmoji.THUMBS_DOWN)], True ) + async def test_instance_method_send_paid_media(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["media"] == "media" + and kwargs["star_count"] == 42 + and kwargs["caption"] == "stars" + ) + + assert check_shortcut_signature(Chat.send_paid_media, Bot.send_paid_media, ["chat_id"], []) + assert await check_shortcut_call(chat.send_paid_media, chat.get_bot(), "send_paid_media") + 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") + def test_mention_html(self): chat = Chat(id=1, type="foo") with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): diff --git a/tests/test_chatfullinfo.py b/tests/test_chatfullinfo.py index b547e4de913..ee9d697ca71 100644 --- a/tests/test_chatfullinfo.py +++ b/tests/test_chatfullinfo.py @@ -55,13 +55,15 @@ def chat_full_info(bot): bio=TestChatFullInfoBase.bio, linked_chat_id=TestChatFullInfoBase.linked_chat_id, location=TestChatFullInfoBase.location, - has_private_forwards=True, - has_protected_content=True, - has_visible_history=True, - join_to_send_messages=True, - join_by_request=True, - has_restricted_voice_and_video_messages=True, - is_forum=True, + has_private_forwards=TestChatFullInfoBase.has_private_forwards, + has_protected_content=TestChatFullInfoBase.has_protected_content, + has_visible_history=TestChatFullInfoBase.has_visible_history, + join_to_send_messages=TestChatFullInfoBase.join_to_send_messages, + join_by_request=TestChatFullInfoBase.join_by_request, + has_restricted_voice_and_video_messages=( + TestChatFullInfoBase.has_restricted_voice_and_video_messages + ), + is_forum=TestChatFullInfoBase.is_forum, active_usernames=TestChatFullInfoBase.active_usernames, emoji_status_custom_emoji_id=TestChatFullInfoBase.emoji_status_custom_emoji_id, emoji_status_expiration_date=TestChatFullInfoBase.emoji_status_expiration_date, @@ -76,10 +78,11 @@ def chat_full_info(bot): business_intro=TestChatFullInfoBase.business_intro, business_location=TestChatFullInfoBase.business_location, business_opening_hours=TestChatFullInfoBase.business_opening_hours, - birthdate=Birthdate(1, 1), + birthdate=TestChatFullInfoBase.birthdate, personal_chat=TestChatFullInfoBase.personal_chat, - first_name="first_name", - last_name="last_name", + first_name=TestChatFullInfoBase.first_name, + last_name=TestChatFullInfoBase.last_name, + can_send_paid_media=TestChatFullInfoBase.can_send_paid_media, ) chat.set_bot(bot) chat._unfreeze() @@ -136,6 +139,7 @@ class TestChatFullInfoBase: personal_chat = Chat(3, "private", "private") first_name = "first_name" last_name = "last_name" + can_send_paid_media = True class TestChatFullInfoWithoutRequest(TestChatFullInfoBase): @@ -188,6 +192,7 @@ def test_de_json(self, bot): "personal_chat": self.personal_chat.to_dict(), "first_name": self.first_name, "last_name": self.last_name, + "can_send_paid_media": self.can_send_paid_media, } cfi = ChatFullInfo.de_json(json_dict, bot) assert cfi.id == self.id_ @@ -232,6 +237,7 @@ def test_de_json(self, bot): assert cfi.first_name == self.first_name assert cfi.last_name == self.last_name assert cfi.max_reaction_count == self.max_reaction_count + assert cfi.can_send_paid_media == self.can_send_paid_media def test_de_json_localization(self, bot, raw_bot, tz_bot): json_dict = { @@ -305,6 +311,7 @@ def test_to_dict(self, chat_full_info): assert cfi_dict["personal_chat"] == cfi.personal_chat.to_dict() assert cfi_dict["first_name"] == cfi.first_name assert cfi_dict["last_name"] == cfi.last_name + assert cfi_dict["can_send_paid_media"] == cfi.can_send_paid_media assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count diff --git a/tests/test_constants.py b/tests/test_constants.py index 75368857325..b750f7fba3a 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -216,6 +216,8 @@ def test_message_attachment_type_completeness_reverse(self): name = to_snake_case(match.group(1)) if name == "photo_size": name = "photo" + if name == "paid_media_info": + name = "paid_media" try: constants.MessageAttachmentType(name) except ValueError: diff --git a/tests/test_message.py b/tests/test_message.py index 8bfc632769d..5596710396d 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -46,6 +46,8 @@ MessageAutoDeleteTimerChanged, MessageEntity, MessageOriginChat, + PaidMediaInfo, + PaidMediaPreview, PassportData, PhotoSize, Poll, @@ -275,6 +277,7 @@ def message(bot): {"chat_background_set": ChatBackground(type=BackgroundTypeChatTheme("ice"))}, {"effect_id": "123456789"}, {"show_caption_above_media": True}, + {"paid_media": PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)])}, ], ids=[ "reply", @@ -346,6 +349,7 @@ def message(bot): "chat_background_set", "effect_id", "show_caption_above_media", + "paid_media", ], ) def message_params(bot, request): @@ -1221,6 +1225,7 @@ def test_effective_attachment(self, message_params): "game", "invoice", "location", + "paid_media", "passport_data", "photo", "poll", diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 99df02b82e7..c9e3b4e4650 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -127,6 +127,8 @@ class ParamTypeCheckingExceptions: "InputTextMessageContent": {"disable_web_page_preview"}, # convenience arg, here for bw compat "RevenueWithdrawalState": {"type"}, # attributes common to all subclasses "TransactionPartner": {"type"}, # attributes common to all subclasses + "PaidMedia": {"type"}, # attributes common to all subclasses + "InputPaidMedia": {"type", "media"}, # attributes common to all subclasses } @@ -153,6 +155,8 @@ def ptb_extra_params(object_name: str) -> set[str]: r"BackgroundFill\w+": {"type"}, r"RevenueWithdrawalState\w+": {"type"}, r"TransactionPartner\w+": {"type"}, + r"PaidMedia\w+": {"type"}, + r"InputPaidMedia\w+": {"type"}, } diff --git a/tests/test_paidmedia.py b/tests/test_paidmedia.py new file mode 100644 index 00000000000..f76bcf6310f --- /dev/null +++ b/tests/test_paidmedia.py @@ -0,0 +1,325 @@ +#!/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/]. + +from copy import deepcopy + +import pytest + +from telegram import ( + Dice, + PaidMedia, + PaidMediaInfo, + PaidMediaPhoto, + PaidMediaPreview, + PaidMediaVideo, + PhotoSize, + Video, +) +from telegram.constants import PaidMediaType +from tests.auxil.slots import mro_slots + + +@pytest.fixture( + scope="module", + params=[ + PaidMedia.PREVIEW, + PaidMedia.PHOTO, + PaidMedia.VIDEO, + ], +) +def pm_scope_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + PaidMediaPreview, + PaidMediaPhoto, + PaidMediaVideo, + ], + ids=[ + PaidMedia.PREVIEW, + PaidMedia.PHOTO, + PaidMedia.VIDEO, + ], +) +def pm_scope_class(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + ( + PaidMediaPreview, + PaidMedia.PREVIEW, + ), + ( + PaidMediaPhoto, + PaidMedia.PHOTO, + ), + ( + PaidMediaVideo, + PaidMedia.VIDEO, + ), + ], + ids=[ + PaidMedia.PREVIEW, + PaidMedia.PHOTO, + PaidMedia.VIDEO, + ], +) +def pm_scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope="module") +def paid_media(pm_scope_class_and_type): + # We use de_json here so that we don't have to worry about which class gets which arguments + return pm_scope_class_and_type[0].de_json( + { + "type": pm_scope_class_and_type[1], + "width": TestPaidMediaBase.width, + "height": TestPaidMediaBase.height, + "duration": TestPaidMediaBase.duration, + "video": TestPaidMediaBase.video.to_dict(), + "photo": [p.to_dict() for p in TestPaidMediaBase.photo], + }, + bot=None, + ) + + +def paid_media_video(): + return PaidMediaVideo(video=TestPaidMediaBase.video) + + +def paid_media_photo(): + return PaidMediaPhoto(photo=TestPaidMediaBase.photo) + + +@pytest.fixture(scope="module") +def paid_media_info(): + return PaidMediaInfo( + star_count=TestPaidMediaInfoBase.star_count, + paid_media=[paid_media_video(), paid_media_photo()], + ) + + +class TestPaidMediaBase: + width = 640 + height = 480 + duration = 60 + video = Video( + file_id="video_file_id", + width=640, + height=480, + file_unique_id="file_unique_id", + duration=60, + ) + photo = ( + PhotoSize( + file_id="photo_file_id", + width=640, + height=480, + file_unique_id="file_unique_id", + ), + ) + + +class TestPaidMediaWithoutRequest(TestPaidMediaBase): + def test_slot_behaviour(self, paid_media): + inst = paid_media + 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, pm_scope_class_and_type): + cls = pm_scope_class_and_type[0] + type_ = pm_scope_class_and_type[1] + + json_dict = { + "type": type_, + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + pm = PaidMedia.de_json(json_dict, bot) + assert set(pm.api_kwargs.keys()) == { + "width", + "height", + "duration", + "video", + "photo", + } - set(cls.__slots__) + + assert isinstance(pm, PaidMedia) + assert type(pm) is cls + assert pm.type == type_ + if "width" in cls.__slots__: + assert pm.width == self.width + assert pm.height == self.height + assert pm.duration == self.duration + if "video" in cls.__slots__: + assert pm.video == self.video + if "photo" in cls.__slots__: + assert pm.photo == self.photo + + assert cls.de_json(None, bot) is None + assert PaidMedia.de_json({}, bot) is None + + def test_de_json_invalid_type(self, bot): + json_dict = { + "type": "invalid", + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + pm = PaidMedia.de_json(json_dict, bot) + assert pm.api_kwargs == { + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + + assert type(pm) is PaidMedia + assert pm.type == "invalid" + + def test_de_json_subclass(self, pm_scope_class, bot): + """This makes sure that e.g. PaidMediaPreivew(data) never returns a + TransactionPartnerPhoto instance.""" + json_dict = { + "type": "invalid", + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + assert type(pm_scope_class.de_json(json_dict, bot)) is pm_scope_class + + def test_to_dict(self, paid_media): + pm_dict = paid_media.to_dict() + + assert isinstance(pm_dict, dict) + assert pm_dict["type"] == paid_media.type + if hasattr(paid_media_info, "width"): + assert pm_dict["width"] == paid_media.width + assert pm_dict["height"] == paid_media.height + assert pm_dict["duration"] == paid_media.duration + if hasattr(paid_media_info, "video"): + assert pm_dict["video"] == paid_media.video.to_dict() + if hasattr(paid_media_info, "photo"): + assert pm_dict["photo"] == [p.to_dict() for p in paid_media.photo] + + def test_type_enum_conversion(self): + assert type(PaidMedia("video").type) is PaidMediaType + assert PaidMedia("unknown").type == "unknown" + + def test_equality(self, paid_media, bot): + a = PaidMedia("base_type") + b = PaidMedia("base_type") + c = paid_media + d = deepcopy(paid_media) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, "video"): + json_dict = c.to_dict() + json_dict["video"] = Video("different", "d2", 1, 1, 1).to_dict() + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + if hasattr(c, "photo"): + json_dict = c.to_dict() + json_dict["photo"] = [PhotoSize("different", "d2", 1, 1, 1).to_dict()] + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + +class TestPaidMediaInfoBase: + star_count = 200 + paid_media = [paid_media_video(), paid_media_photo()] + + +class TestPaidMediaInfoWithoutRequest(TestPaidMediaInfoBase): + def test_slot_behaviour(self, paid_media_info): + inst = paid_media_info + 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 = { + "star_count": self.star_count, + "paid_media": [t.to_dict() for t in self.paid_media], + } + pmi = PaidMediaInfo.de_json(json_dict, bot) + pmi_none = PaidMediaInfo.de_json(None, bot) + assert pmi.paid_media == tuple(self.paid_media) + assert pmi.star_count == self.star_count + assert pmi_none is None + + def test_to_dict(self, paid_media_info): + assert paid_media_info.to_dict() == { + "star_count": self.star_count, + "paid_media": [t.to_dict() for t in self.paid_media], + } + + def test_equality(self): + pmi1 = PaidMediaInfo( + star_count=self.star_count, paid_media=[paid_media_video(), paid_media_photo()] + ) + pmi2 = PaidMediaInfo( + star_count=self.star_count, paid_media=[paid_media_video(), paid_media_photo()] + ) + pmi3 = PaidMediaInfo(star_count=100, paid_media=[paid_media_photo()]) + + assert pmi1 == pmi2 + assert hash(pmi1) == hash(pmi2) + + assert pmi1 != pmi3 + assert hash(pmi1) != hash(pmi3) diff --git a/tests/test_reply.py b/tests/test_reply.py index 4d2c35e8d31..f41ed01eb59 100644 --- a/tests/test_reply.py +++ b/tests/test_reply.py @@ -29,6 +29,8 @@ LinkPreviewOptions, MessageEntity, MessageOriginUser, + PaidMediaInfo, + PaidMediaPreview, ReplyParameters, TextQuote, User, @@ -44,6 +46,7 @@ def external_reply_info(): message_id=TestExternalReplyInfoBase.message_id, link_preview_options=TestExternalReplyInfoBase.link_preview_options, giveaway=TestExternalReplyInfoBase.giveaway, + paid_media=TestExternalReplyInfoBase.paid_media, ) @@ -59,6 +62,7 @@ class TestExternalReplyInfoBase: dtm.datetime.now(dtm.timezone.utc).replace(microsecond=0), 1, ) + paid_media = PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)]) class TestExternalReplyInfoWithoutRequest(TestExternalReplyInfoBase): @@ -76,6 +80,7 @@ def test_de_json(self, bot): "message_id": self.message_id, "link_preview_options": self.link_preview_options.to_dict(), "giveaway": self.giveaway.to_dict(), + "paid_media": self.paid_media.to_dict(), } external_reply_info = ExternalReplyInfo.de_json(json_dict, bot) @@ -86,6 +91,7 @@ def test_de_json(self, bot): assert external_reply_info.message_id == self.message_id assert external_reply_info.link_preview_options == self.link_preview_options assert external_reply_info.giveaway == self.giveaway + assert external_reply_info.paid_media == self.paid_media assert ExternalReplyInfo.de_json(None, bot) is None @@ -98,6 +104,7 @@ def test_to_dict(self, external_reply_info): assert ext_reply_info_dict["message_id"] == self.message_id assert ext_reply_info_dict["link_preview_options"] == self.link_preview_options.to_dict() assert ext_reply_info_dict["giveaway"] == self.giveaway.to_dict() + assert ext_reply_info_dict["paid_media"] == self.paid_media.to_dict() def test_equality(self, external_reply_info): a = external_reply_info diff --git a/tests/test_stars.py b/tests/test_stars.py index 25567b30cc0..fb1339a7217 100644 --- a/tests/test_stars.py +++ b/tests/test_stars.py @@ -33,6 +33,7 @@ TransactionPartner, TransactionPartnerFragment, TransactionPartnerOther, + TransactionPartnerTelegramAds, TransactionPartnerUser, User, ) @@ -101,6 +102,7 @@ def star_transactions(): TransactionPartner.FRAGMENT, TransactionPartner.OTHER, TransactionPartner.USER, + TransactionPartner.TELEGRAM_ADS, ], ) def tp_scope_type(request): @@ -113,11 +115,13 @@ def tp_scope_type(request): TransactionPartnerFragment, TransactionPartnerOther, TransactionPartnerUser, + TransactionPartnerTelegramAds, ], ids=[ TransactionPartner.FRAGMENT, TransactionPartner.OTHER, TransactionPartner.USER, + TransactionPartner.TELEGRAM_ADS, ], ) def tp_scope_class(request): @@ -130,11 +134,13 @@ def tp_scope_class(request): (TransactionPartnerFragment, TransactionPartner.FRAGMENT), (TransactionPartnerOther, TransactionPartner.OTHER), (TransactionPartnerUser, TransactionPartner.USER), + (TransactionPartnerTelegramAds, TransactionPartner.TELEGRAM_ADS), ], ids=[ TransactionPartner.FRAGMENT, TransactionPartner.OTHER, TransactionPartner.USER, + TransactionPartner.TELEGRAM_ADS, ], ) def tp_scope_class_and_type(request): @@ -147,6 +153,7 @@ def transaction_partner(tp_scope_class_and_type): return tp_scope_class_and_type[0].de_json( { "type": tp_scope_class_and_type[1], + "invoice_payload": TestTransactionPartnerBase.invoice_payload, "withdrawal_state": TestTransactionPartnerBase.withdrawal_state.to_dict(), "user": TestTransactionPartnerBase.user.to_dict(), }, @@ -244,6 +251,7 @@ def test_de_json(self, bot): } st = StarTransaction.de_json(json_dict, bot) st_none = StarTransaction.de_json(None, bot) + assert st.api_kwargs == {} assert st.id == self.id assert st.amount == self.amount assert st.date == from_timestamp(self.date) @@ -329,6 +337,7 @@ def test_de_json(self, bot): } st = StarTransactions.de_json(json_dict, bot) st_none = StarTransactions.de_json(None, bot) + assert st.api_kwargs == {} assert st.transactions == tuple(self.transactions) assert st_none is None @@ -359,6 +368,7 @@ def test_equality(self): class TestTransactionPartnerBase: withdrawal_state = withdrawal_state_succeeded() user = transaction_partner_user().user + invoice_payload = "payload" class TestTransactionPartnerWithoutRequest(TestTransactionPartnerBase): @@ -374,11 +384,14 @@ def test_de_json(self, bot, tp_scope_class_and_type): json_dict = { "type": type_, + "invoice_payload": self.invoice_payload, "withdrawal_state": self.withdrawal_state.to_dict(), "user": self.user.to_dict(), } tp = TransactionPartner.de_json(json_dict, bot) - assert set(tp.api_kwargs.keys()) == {"user", "withdrawal_state"} - set(cls.__slots__) + assert set(tp.api_kwargs.keys()) == {"user", "withdrawal_state", "invoice_payload"} - set( + cls.__slots__ + ) assert isinstance(tp, TransactionPartner) assert type(tp) is cls @@ -387,6 +400,7 @@ def test_de_json(self, bot, tp_scope_class_and_type): assert tp.withdrawal_state == self.withdrawal_state if "user" in cls.__slots__: assert tp.user == self.user + assert tp.invoice_payload == self.invoice_payload assert cls.de_json(None, bot) is None assert TransactionPartner.de_json({}, bot) is None @@ -394,6 +408,7 @@ def test_de_json(self, bot, tp_scope_class_and_type): def test_de_json_invalid_type(self, bot): json_dict = { "type": "invalid", + "invoice_payload": self.invoice_payload, "withdrawal_state": self.withdrawal_state.to_dict(), "user": self.user.to_dict(), } @@ -401,6 +416,7 @@ def test_de_json_invalid_type(self, bot): assert tp.api_kwargs == { "withdrawal_state": self.withdrawal_state.to_dict(), "user": self.user.to_dict(), + "invoice_payload": self.invoice_payload, } assert type(tp) is TransactionPartner @@ -411,6 +427,7 @@ def test_de_json_subclass(self, tp_scope_class, bot): TransactionPartnerFragment instance.""" json_dict = { "type": "invalid", + "invoice_payload": self.invoice_payload, "withdrawal_state": self.withdrawal_state.to_dict(), "user": self.user.to_dict(), } @@ -423,6 +440,7 @@ def test_to_dict(self, transaction_partner): assert tp_dict["type"] == transaction_partner.type if hasattr(transaction_partner, "user"): assert tp_dict["user"] == transaction_partner.user.to_dict() + assert tp_dict["invoice_payload"] == transaction_partner.invoice_payload if hasattr(transaction_partner, "withdrawal_state"): assert tp_dict["withdrawal_state"] == transaction_partner.withdrawal_state.to_dict()