diff --git a/pyrogram/client/methods/messages/forward_messages.py b/pyrogram/client/methods/messages/forward_messages.py index 35d406ae..39db4bee 100644 --- a/pyrogram/client/methods/messages/forward_messages.py +++ b/pyrogram/client/methods/messages/forward_messages.py @@ -29,7 +29,9 @@ class ForwardMessages(BaseClient): chat_id: Union[int, str], from_chat_id: Union[int, str], message_ids: Iterable[int], - disable_notification: bool = None + disable_notification: bool = None, + as_copy: bool = False, + remove_caption: bool = False ) -> "pyrogram.Messages": """Use this method to forward messages of any kind. @@ -52,6 +54,15 @@ class ForwardMessages(BaseClient): Sends the message silently. Users will receive a notification with no sound. + as_copy (``bool``, *optional*): + Pass True to forward messages without the forward header (i.e.: send a copy of the message content). + Defaults to False. + + remove_caption (``bool``, *optional*): + If set to True and *as_copy* is enabled as well, media captions are not preserved when copying the + message. Has no effect if *as_copy* is not enabled. + Defaults to False. + Returns: On success and in case *message_ids* was an iterable, the returned value will be a list of the forwarded :obj:`Messages ` even if a list contains just one element, otherwise if @@ -61,35 +72,55 @@ class ForwardMessages(BaseClient): Raises: :class:`Error ` in case of a Telegram RPC error. """ + is_iterable = not isinstance(message_ids, int) message_ids = list(message_ids) if is_iterable else [message_ids] - r = self.send( - functions.messages.ForwardMessages( - to_peer=self.resolve_peer(chat_id), - from_peer=self.resolve_peer(from_chat_id), - id=message_ids, - silent=disable_notification or None, - random_id=[self.rnd_id() for _ in message_ids] - ) - ) - - messages = [] - - users = {i.id: i for i in r.users} - chats = {i.id: i for i in r.chats} - - for i in r.updates: - if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)): - messages.append( - pyrogram.Message._parse( - self, i.message, - users, chats + if as_copy: + sent_messages = [] + for chunk in [message_ids[i:i + 200] for i in range(0, len(message_ids), 200)]: + messages = self.get_messages(chat_id=from_chat_id, message_ids=chunk) # type: pyrogram.Messages + for message in messages.messages: + sent_messages.append( + message.forward( + chat_id, + disable_notification=disable_notification, + as_copy=True, + remove_caption=remove_caption + ) ) + return pyrogram.Messages( + client=self, + total_count=len(sent_messages), + messages=sent_messages + ) if is_iterable else sent_messages[0] + else: + r = self.send( + functions.messages.ForwardMessages( + to_peer=self.resolve_peer(chat_id), + from_peer=self.resolve_peer(from_chat_id), + id=message_ids, + silent=disable_notification or None, + random_id=[self.rnd_id() for _ in message_ids] ) + ) - return pyrogram.Messages( - client=self, - total_count=len(messages), - messages=messages - ) if is_iterable else messages[0] + forwarded_messages = [] + + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + + for i in r.updates: + if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)): + forwarded_messages.append( + pyrogram.Message._parse( + self, i.message, + users, chats + ) + ) + + return pyrogram.Messages( + client=self, + total_count=len(forwarded_messages), + messages=forwarded_messages + ) if is_iterable else forwarded_messages[0] diff --git a/pyrogram/client/methods/messages/send_contact.py b/pyrogram/client/methods/messages/send_contact.py index bed9a37e..14ce61ec 100644 --- a/pyrogram/client/methods/messages/send_contact.py +++ b/pyrogram/client/methods/messages/send_contact.py @@ -29,8 +29,8 @@ class SendContact(BaseClient): chat_id: Union[int, str], phone_number: str, first_name: str, - last_name: str = "", - vcard: str = "", + last_name: str = None, + vcard: str = None, disable_notification: bool = None, reply_to_message_id: int = None, reply_markup: Union[ @@ -83,8 +83,8 @@ class SendContact(BaseClient): media=types.InputMediaContact( phone_number=phone_number, first_name=first_name, - last_name=last_name, - vcard=vcard + last_name=last_name or "", + vcard=vcard or "" ), message="", silent=disable_notification or None, diff --git a/pyrogram/client/style/html.py b/pyrogram/client/style/html.py index 040e770b..211716e1 100644 --- a/pyrogram/client/style/html.py +++ b/pyrogram/client/style/html.py @@ -40,7 +40,7 @@ class HTML: def parse(self, message: str): entities = [] - message = utils.add_surrogates(str(message)) + message = utils.add_surrogates(str(message or "")) offset = 0 for match in self.HTML_RE.finditer(message): diff --git a/pyrogram/client/style/markdown.py b/pyrogram/client/style/markdown.py index 04ce95c8..45037a35 100644 --- a/pyrogram/client/style/markdown.py +++ b/pyrogram/client/style/markdown.py @@ -56,7 +56,7 @@ class Markdown: self.peers_by_id = peers_by_id def parse(self, message: str): - message = utils.add_surrogates(str(message)).strip() + message = utils.add_surrogates(str(message or "")).strip() entities = [] offset = 0 diff --git a/pyrogram/client/types/messages_and_media/message.py b/pyrogram/client/types/messages_and_media/message.py index 558efd38..4ead0d30 100644 --- a/pyrogram/client/types/messages_and_media/message.py +++ b/pyrogram/client/types/messages_and_media/message.py @@ -16,12 +16,13 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +from functools import partial from typing import List, Match, Union import pyrogram from pyrogram.api import types from pyrogram.api.errors import MessageIdsEmpty -from pyrogram.client.ext import ChatAction +from pyrogram.client.ext import ChatAction, ParseMode from pyrogram.client.types.input_media import InputMedia from .contact import Contact from .location import Location @@ -33,6 +34,32 @@ from ..user_and_chats.chat import Chat from ..user_and_chats.user import User +class Str(str): + def __init__(self, *args): + super().__init__() + + self._client = None + self._entities = None + + def init(self, client, entities): + self._client = client + self._entities = entities + + return self + + @property + def text(self): + return self + + @property + def markdown(self): + return self._client.markdown.unparse(self, self._entities) + + @property + def html(self): + return self._client.html.unparse(self, self._entities) + + class Message(PyrogramType, Update): """This object represents a message. @@ -268,7 +295,7 @@ class Message(PyrogramType, Update): edit_date: int = None, media_group_id: str = None, author_signature: str = None, - text: str = None, + text: Str = None, entities: List["pyrogram.MessageEntity"] = None, caption_entities: List["pyrogram.MessageEntity"] = None, audio: "pyrogram.Audio" = None, @@ -280,7 +307,7 @@ class Message(PyrogramType, Update): video: "pyrogram.Video" = None, voice: "pyrogram.Voice" = None, video_note: "pyrogram.VideoNote" = None, - caption: str = None, + caption: Str = None, contact: "pyrogram.Contact" = None, location: "pyrogram.Location" = None, venue: "pyrogram.Venue" = None, @@ -2519,7 +2546,13 @@ class Message(PyrogramType, Update): reply_markup=reply_markup ) - def forward(self, chat_id: int or str, disable_notification: bool = None) -> "Message": + def forward( + self, + chat_id: int or str, + disable_notification: bool = None, + as_copy: bool = False, + remove_caption: bool = False + ) -> "Message": """Bound method *forward* of :obj:`Message `. Use as a shortcut for: @@ -2547,18 +2580,120 @@ class Message(PyrogramType, Update): Sends the message silently. Users will receive a notification with no sound. + as_copy (``bool``, *optional*): + Pass True to forward messages without the forward header (i.e.: send a copy of the message content). + Defaults to False. + + remove_caption (``bool``, *optional*): + If set to True and *as_copy* is enabled as well, media captions are not preserved when copying the + message. Has no effect if *as_copy* is not enabled. + Defaults to False. + Returns: On success, the forwarded Message is returned. Raises: :class:`Error ` """ - return self._client.forward_messages( - chat_id=chat_id, - from_chat_id=self.chat.id, - message_ids=self.message_id, - disable_notification=disable_notification - ) + if as_copy: + if self.service: + raise ValueError("Unable to copy service messages") + + if self.game and not self._client.is_bot: + raise ValueError("Users cannot send messages with Game media type") + + # TODO: Improve markdown parser. Currently html appears to be more stable, thus we use it here because users + # can"t choose. + + if self.text: + return self._client.send_message( + chat_id, + text=self.text.html, + parse_mode="html", + disable_web_page_preview=not self.web_page, + disable_notification=disable_notification + ) + elif self.media: + caption = self.caption.html if self.caption and not remove_caption else None + + send_media = partial( + self._client.send_cached_media, + chat_id=chat_id, + disable_notification=disable_notification + ) + + if self.photo: + file_id = self.photo.sizes[-1].file_id + elif self.audio: + file_id = self.audio.file_id + elif self.document: + file_id = self.document.file_id + elif self.video: + file_id = self.video.file_id + elif self.animation: + file_id = self.animation.file_id + elif self.voice: + file_id = self.voice.file_id + elif self.sticker: + file_id = self.sticker.file_id + elif self.video_note: + file_id = self.video_note.file_id + elif self.contact: + return self._client.send_contact( + chat_id, + phone_number=self.contact.phone_number, + first_name=self.contact.first_name, + last_name=self.contact.last_name, + vcard=self.contact.vcard, + disable_notification=disable_notification + ) + elif self.location: + return self._client.send_location( + chat_id, + latitude=self.location.latitude, + longitude=self.location.longitude, + disable_notification=disable_notification + ) + elif self.venue: + return self._client.send_venue( + chat_id, + latitude=self.venue.location.latitude, + longitude=self.venue.location.longitude, + title=self.venue.title, + address=self.venue.address, + foursquare_id=self.venue.foursquare_id, + foursquare_type=self.venue.foursquare_type, + disable_notification=disable_notification + ) + elif self.poll: + return self._client.send_poll( + chat_id, + question=self.poll.question, + options=[opt.text for opt in self.poll.options], + disable_notification=disable_notification + ) + elif self.game: + return self._client.send_game( + chat_id, + game_short_name=self.game.short_name, + disable_notification=disable_notification + ) + else: + raise ValueError("Unknown media type") + + if self.sticker or self.video_note: # Sticker and VideoNote should have no caption + return send_media(file_id) + else: + return send_media(file_id=file_id, caption=caption, parse_mode=ParseMode.HTML) + else: + raise ValueError("Can't copy this message") + else: + return self._client.forward_messages( + chat_id=chat_id, + from_chat_id=self.chat.id, + message_ids=self.message_id, + disable_notification=disable_notification + ) def delete(self, revoke: bool = True): """Bound method *delete* of :obj:`Message `. @@ -2798,29 +2933,3 @@ class Message(PyrogramType, Update): message_id=self.message_id, disable_notification=disable_notification ) - - -class Str(str): - def __init__(self, *args): - super().__init__() - - self.client = None - self.entities = None - - def init(self, client, entities): - self.client = client - self.entities = entities - - return self - - @property - def text(self): - return self - - @property - def markdown(self): - return self.client.markdown.unparse(self, self.entities) - - @property - def html(self): - return self.client.html.unparse(self, self.entities) diff --git a/pyrogram/client/types/messages_and_media/messages.py b/pyrogram/client/types/messages_and_media/messages.py index 9983a01d..aae31a82 100644 --- a/pyrogram/client/types/messages_and_media/messages.py +++ b/pyrogram/client/types/messages_and_media/messages.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import List +from typing import List, Union import pyrogram from pyrogram.api import types @@ -116,3 +116,55 @@ class Messages(PyrogramType, Update): messages=parsed_messages, client=client ) + + def forward( + self, + chat_id: Union[int, str], + disable_notification: bool = None, + as_copy: bool = False, + remove_caption: bool = False + ): + """Bound method *forward* of :obj:`Message `. + + Args: + 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). + + disable_notification (``bool``, *optional*): + Sends messages silently. + Users will receive a notification with no sound. + + as_copy (``bool``, *optional*): + Pass True to forward messages without the forward header (i.e.: send a copy of the message content). + Defaults to False. + + remove_caption (``bool``, *optional*): + If set to True and *as_copy* is enabled as well, media captions are not preserved when copying the + message. Has no effect if *as_copy* is not enabled. + Defaults to False. + + Returns: + On success, a :class:`Messages ` containing forwarded messages is returned. + + Raises: + :class:`Error ` + """ + forwarded_messages = [] + + for message in self.messages: + forwarded_messages.append( + message.forward( + chat_id=chat_id, + as_copy=as_copy, + disable_notification=disable_notification, + remove_caption=remove_caption + ) + ) + + return Messages( + total_count=len(forwarded_messages), + messages=forwarded_messages, + client=self._client + ) diff --git a/pyrogram/client/types/pyrogram_type.py b/pyrogram/client/types/pyrogram_type.py index d746e6a7..af828926 100644 --- a/pyrogram/client/types/pyrogram_type.py +++ b/pyrogram/client/types/pyrogram_type.py @@ -19,11 +19,13 @@ from collections import OrderedDict from json import dumps +import pyrogram + class PyrogramType: __slots__ = ["_client"] - def __init__(self, client): + def __init__(self, client: "pyrogram.client.ext.BaseClient"): self._client = client def __str__(self):