diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index 90c1e2a3..5dabc668 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -205,6 +205,7 @@ def pyrogram_api(): start_bot update_color delete_chat_history + send_paid_media """, chats=""" Chats @@ -523,6 +524,8 @@ def pyrogram_api(): GiftCode CheckedGiftCode SuccessfulPayment + PaidMediaInfo + PaidMediaPreview """, bot_keyboards=""" Bot keyboards diff --git a/pyrogram/enums/message_media_type.py b/pyrogram/enums/message_media_type.py index 0d93ceec..b7df9a3b 100644 --- a/pyrogram/enums/message_media_type.py +++ b/pyrogram/enums/message_media_type.py @@ -80,3 +80,6 @@ class MessageMediaType(AutoName): INVOICE = auto() "Invoice media" + + PAID_MEDIA = auto() + "Paid media" diff --git a/pyrogram/methods/messages/__init__.py b/pyrogram/methods/messages/__init__.py index 0828ca50..fd56a6ea 100644 --- a/pyrogram/methods/messages/__init__.py +++ b/pyrogram/methods/messages/__init__.py @@ -61,6 +61,7 @@ from .send_document import SendDocument from .send_location import SendLocation from .send_media_group import SendMediaGroup from .send_message import SendMessage +from .send_paid_media import SendPaidMedia from .send_photo import SendPhoto from .send_poll import SendPoll from .send_reaction import SendReaction @@ -97,6 +98,7 @@ class Messages( SendLocation, SendMediaGroup, SendMessage, + SendPaidMedia, SendPhoto, SendSticker, SendVenue, diff --git a/pyrogram/methods/messages/send_paid_media.py b/pyrogram/methods/messages/send_paid_media.py new file mode 100644 index 00000000..ea6b9165 --- /dev/null +++ b/pyrogram/methods/messages/send_paid_media.py @@ -0,0 +1,282 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +import logging +import os +from datetime import datetime +from typing import Union, List, Optional + +import pyrogram +from pyrogram import raw +from pyrogram import types +from pyrogram import utils +from pyrogram import enums +from pyrogram.file_id import FileType + +log = logging.getLogger(__name__) + + +class SendPaidMedia: + # TODO: Add progress parameter + async def send_paid_media( + self: "pyrogram.Client", + chat_id: Union[int, str], + stars_amount: int, + media: List[Union[ + "types.InputMediaPhoto", + "types.InputMediaVideo", + ]], + caption: str = "", + parse_mode: Optional["enums.ParseMode"] = None, + caption_entities: List["types.MessageEntity"] = None, + disable_notification: bool = None, + reply_to_message_id: int = None, + quote_text: str = None, + quote_entities: List["types.MessageEntity"] = None, + quote_offset: int = None, + schedule_date: datetime = None, + protect_content: bool = None, + show_above_text: bool = None + ) -> List["types.Message"]: + """Send a group or one paid photo/video. + + .. include:: /_includes/usable-by/users-bots.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For your personal cloud (Saved Messages) you can simply use "me" or "self". + For a contact that exists in your Telegram address book you can use his phone number (str). + + stars_amount (``int``): + The number of Telegram Stars that must be paid to buy access to the media. + + media (List of :obj:`~pyrogram.types.InputMediaPhoto`, :obj:`~pyrogram.types.InputMediaVideo`): + A list describing photos and videos to be sent, must include 1–10 items. + + caption (``str``, *optional*): + Media caption, 0-1024 characters after entities parsing. + + parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + + disable_notification (``bool``, *optional*): + Sends the message silently. + Users will receive a notification with no sound. + + reply_to_message_id (``int``, *optional*): + If the message is a reply, ID of the original message. + + quote_text (``str``, *optional*): + Text of the quote to be sent. + + parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + + quote_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): + List of special entities that appear in quote text, which can be specified instead of *parse_mode*. + + quote_offset (``int``, *optional*): + Offset for quote in original message. + + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. + + protect_content (``bool``, *optional*): + Protects the contents of the sent message from forwarding and saving. + + show_above_text (``bool``, *optional*): + If True, link preview will be shown above the message text. + Otherwise, the link preview will be shown below the message text. + + Returns: + :obj:`~pyrogram.types.Message`: On success, the sent message is returned. + + Example: + .. code-block:: python + + from pyrogram.types import InputMediaPhoto, InputMediaVideo + + await app.send_paid_media( + chat_id, + stars_amount=50, + caption="Look at this!", + media=[ + InputMediaPhoto("photo1.jpg"), + InputMediaPhoto("photo2.jpg"), + InputMediaVideo("video.mp4") + ] + ) + """ + multi_media = [] + + for i in media: + if isinstance(i, types.InputMediaPhoto): + if isinstance(i.media, str): + if os.path.isfile(i.media): + media = await self.invoke( + raw.functions.messages.UploadMedia( + peer=await self.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedPhoto( + file=await self.save_file(i.media), + spoiler=i.has_spoiler + ), + ) + ) + + media = raw.types.InputMediaPhoto( + id=raw.types.InputPhoto( + id=media.photo.id, + access_hash=media.photo.access_hash, + file_reference=media.photo.file_reference + ), + spoiler=i.has_spoiler + ) + else: + media = utils.get_input_media_from_file_id(i.media, FileType.PHOTO, has_spoiler=i.has_spoiler) + else: + media = await self.invoke( + raw.functions.messages.UploadMedia( + peer=await self.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedPhoto( + file=await self.save_file(i.media), + spoiler=i.has_spoiler + ), + ) + ) + + media = raw.types.InputMediaPhoto( + id=raw.types.InputPhoto( + id=media.photo.id, + access_hash=media.photo.access_hash, + file_reference=media.photo.file_reference + ), + spoiler=i.has_spoiler + ) + elif isinstance(i, types.InputMediaVideo): + if isinstance(i.media, str): + if os.path.isfile(i.media): + media = await self.invoke( + raw.functions.messages.UploadMedia( + peer=await self.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedDocument( + file=await self.save_file(i.media), + thumb=await self.save_file(i.thumb), + spoiler=i.has_spoiler, + mime_type=self.guess_mime_type(i.media) or "video/mp4", + nosound_video=True, + attributes=[ + raw.types.DocumentAttributeVideo( + supports_streaming=i.supports_streaming or None, + duration=i.duration, + w=i.width, + h=i.height + ), + raw.types.DocumentAttributeFilename(file_name=os.path.basename(i.media)) + ] + ), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference + ), + spoiler=i.has_spoiler + ) + else: + media = utils.get_input_media_from_file_id(i.media, FileType.VIDEO, has_spoiler=i.has_spoiler) + else: + media = await self.invoke( + raw.functions.messages.UploadMedia( + peer=await self.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedDocument( + file=await self.save_file(i.media), + thumb=await self.save_file(i.thumb), + spoiler=i.has_spoiler, + mime_type=self.guess_mime_type(getattr(i.media, "name", "video.mp4")) or "video/mp4", + nosound_video=True, + attributes=[ + raw.types.DocumentAttributeVideo( + supports_streaming=i.supports_streaming or None, + duration=i.duration, + w=i.width, + h=i.height + ), + raw.types.DocumentAttributeFilename(file_name=getattr(i.media, "name", "video.mp4")) + ] + ), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference + ), + spoiler=i.has_spoiler + ) + else: + raise ValueError(f"{i.__class__.__name__} is not a supported type for send_paid_media") + + multi_media.append(media) + + quote_text, quote_entities = (await utils.parse_text_entities(self, quote_text, parse_mode, quote_entities)).values() + + r = await self.invoke( + raw.functions.messages.SendMedia( + peer=await self.resolve_peer(chat_id), + media=raw.types.InputMediaPaidMedia( + stars_amount=stars_amount, + extended_media=multi_media + ), + silent=disable_notification or None, + reply_to=utils.get_reply_to( + reply_to_message_id=reply_to_message_id, + quote_text=quote_text, + quote_entities=quote_entities, + quote_offset=quote_offset, + ), + random_id=self.rnd_id(), + schedule_date=utils.datetime_to_timestamp(schedule_date), + noforwards=protect_content, + invert_media=show_above_text, + **await utils.parse_text_entities(self, caption, parse_mode, caption_entities) + ), + sleep_threshold=60, + ) + + return await utils.parse_messages( + self, + raw.types.messages.Messages( + messages=[m.message for m in filter( + lambda u: isinstance(u, (raw.types.UpdateNewMessage, + raw.types.UpdateNewChannelMessage, + raw.types.UpdateNewScheduledMessage, + raw.types.UpdateBotNewBusinessMessage)), + r.updates + )], + users=r.users, + chats=r.chats + ), + ) diff --git a/pyrogram/types/messages_and_media/__init__.py b/pyrogram/types/messages_and_media/__init__.py index 00b46d9d..59b96359 100644 --- a/pyrogram/types/messages_and_media/__init__.py +++ b/pyrogram/types/messages_and_media/__init__.py @@ -42,6 +42,8 @@ from .message import Message from .message_entity import MessageEntity from .message_reactions import MessageReactions from .my_boost import MyBoost +from .paid_media_info import PaidMediaInfo +from .paid_media_preview import PaidMediaPreview from .photo import Photo from .poll import Poll from .poll_option import PollOption @@ -85,6 +87,8 @@ __all__ = [ "MessageEntity", "MessageReactions", "MyBoost", + "PaidMediaInfo", + "PaidMediaPreview", "Photo", "Poll", "PollOption", diff --git a/pyrogram/types/messages_and_media/message.py b/pyrogram/types/messages_and_media/message.py index a8402626..926c6e16 100644 --- a/pyrogram/types/messages_and_media/message.py +++ b/pyrogram/types/messages_and_media/message.py @@ -143,6 +143,9 @@ class Message(Object, Update): This field will contain the enumeration type of the media message. You can use ``media = getattr(message, message.media.value)`` to access the media message. + paid_media (:obj:`~pyrogram.types.PaidMediaInfo`, *optional*): + The message is a paid media message. + show_above_text (``bool``, *optional*): If True, link preview will be shown above the message text. Otherwise, the link preview will be shown below the message text. @@ -437,6 +440,7 @@ class Message(Object, Update): scheduled: bool = None, from_scheduled: bool = None, media: "enums.MessageMediaType" = None, + paid_media: "types.PaidMediaInfo" = None, show_above_text: bool = None, edit_date: datetime = None, edit_hidden: bool = None, @@ -545,6 +549,7 @@ class Message(Object, Update): self.scheduled = scheduled self.from_scheduled = from_scheduled self.media = media + self.paid_media = paid_media self.show_above_text = show_above_text self.edit_date = edit_date self.edit_hidden = edit_hidden @@ -918,6 +923,7 @@ class Message(Object, Update): web_page = None poll = None dice = None + paid_media = None media = message.media media_type = None @@ -1020,6 +1026,9 @@ class Message(Object, Update): elif isinstance(media, raw.types.MessageMediaDice): dice = types.Dice._parse(client, media) media_type = enums.MessageMediaType.DICE + elif isinstance(media, raw.types.MessageMediaPaidMedia): + paid_media = types.PaidMediaInfo._parse(client, media) + media_type = enums.MessageMediaType.PAID else: media = None @@ -1083,6 +1092,7 @@ class Message(Object, Update): scheduled=is_scheduled, from_scheduled=message.from_scheduled, media=media_type, + paid_media=paid_media, show_above_text=getattr(message, "invert_media", None), edit_date=utils.timestamp_to_datetime(message.edit_date), edit_hidden=message.edit_hide, diff --git a/pyrogram/types/messages_and_media/paid_media_info.py b/pyrogram/types/messages_and_media/paid_media_info.py new file mode 100644 index 00000000..4bf8bba1 --- /dev/null +++ b/pyrogram/types/messages_and_media/paid_media_info.py @@ -0,0 +1,93 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import List, Union + +import pyrogram +from pyrogram import raw, types +from ..object import Object + + +class PaidMediaInfo(Object): + """Describes the paid media added to a message. + + Parameters: + stars_amount (``int``): + The number of Telegram Stars that must be paid to buy access to the media. + + media (List of :obj:`~pyrogram.types.Photo` | :obj:`~pyrogram.types.Video` | :obj:`~pyrogram.types.PaidMediaPreview`): + Information about the paid media. + """ + + def __init__( + self, + *, + stars_amount: str, + media: List[Union["types.Photo", "types.Video", "types.PaidMediaPreview"]] + ): + super().__init__() + + self.stars_amount = stars_amount + self.media = media + + @staticmethod + def _parse( + client: "pyrogram.Client", + message_paid_media: "raw.types.MessageMediaPaidMedia" + ) -> "PaidMediaInfo": + medias = [] + + for extended_media in message_paid_media.extended_media: + if isinstance(extended_media, raw.types.MessageExtendedMediaPreview): + thumbnail = None + + if isinstance(getattr(extended_media, "thumb", None), raw.types.PhotoStrippedSize): + thumbnail = types.StrippedThumbnail._parse(client, extended_media.thumb) + + medias.append( + types.PaidMediaPreview( + width=getattr(extended_media, "w", None), + height=getattr(extended_media, "h", None), + duration=getattr(extended_media, "video_duration", None), + thumbnail=thumbnail, + ) + ) + elif isinstance(extended_media, raw.types.MessageExtendedMedia): + media = extended_media.media + + if isinstance(media, raw.types.MessageMediaPhoto): + medias.append(types.Photo._parse(client, media.photo, media.ttl_seconds)) + elif isinstance(media, raw.types.MessageMediaDocument): + doc = media.document + + attributes = {type(i): i for i in doc.attributes} + + file_name = getattr( + attributes.get( + raw.types.DocumentAttributeFilename, None + ), "file_name", None + ) + + video_attributes = attributes[raw.types.DocumentAttributeVideo] + + medias.append(types.Video._parse(client, doc, video_attributes, file_name, media.ttl_seconds)) + + return PaidMediaInfo( + stars_amount=message_paid_media.stars_amount, + media=types.List(medias) + ) diff --git a/pyrogram/types/messages_and_media/paid_media_preview.py b/pyrogram/types/messages_and_media/paid_media_preview.py new file mode 100644 index 00000000..11d1d020 --- /dev/null +++ b/pyrogram/types/messages_and_media/paid_media_preview.py @@ -0,0 +1,55 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram import types + +from ..object import Object + + +class PaidMediaPreview(Object): + """The paid media isn't available before the payment. + + Parameters: + width (``int``, *optional*): + Media width as defined by the sender. + + height (``int``, *optional*): + Media height as defined by the sender. + + duration (``int``, *optional*): + Duration of the media in seconds as defined by the sender. + + thumbnail (:obj:`~pyrogram.types.StrippedThumbnail`, *optional*): + Media thumbnail. + + """ + + def __init__( + self, + *, + width: int = None, + height: int = None, + duration: int = None, + thumbnail: "types.StrippedThumbnail" = None + ): + super().__init__() + + self.width = width + self.height = height + self.duration = duration + self.thumbnail = thumbnail