From aa704364414d6af6295b6720e5678ad03e703d5e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 10:32:32 +0200 Subject: [PATCH 01/15] Add a custom Message class --- telethon/tl/custom/__init__.py | 2 + telethon/tl/custom/message.py | 197 ++++++++++++++++++++++++++++ telethon/tl/custom/messagebutton.py | 69 ++++++++++ 3 files changed, 268 insertions(+) create mode 100644 telethon/tl/custom/message.py create mode 100644 telethon/tl/custom/messagebutton.py diff --git a/telethon/tl/custom/__init__.py b/telethon/tl/custom/__init__.py index f74189f6..1d3f4068 100644 --- a/telethon/tl/custom/__init__.py +++ b/telethon/tl/custom/__init__.py @@ -1,3 +1,5 @@ from .draft import Draft from .dialog import Dialog from .input_sized_file import InputSizedFile +from .messagebutton import MessageButton +from .message import Message diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py new file mode 100644 index 00000000..d36e4f0d --- /dev/null +++ b/telethon/tl/custom/message.py @@ -0,0 +1,197 @@ +from .. import types +from ...extensions import markdown +from ...utils import get_peer_id +from .messagebutton import MessageButton + + +class Message: + """ + Custom class that encapsulates a message providing an abstraction to + easily access some commonly needed features (such as the markdown text + or the text for a given message entity). + + Attributes: + + original_message (:tl:`Message`): + The original :tl:`Message` object. + + Any other attribute: + Attributes not described here are the same as those available + in the original :tl:`Message`. + """ + def __init__(self, client, original, entities=None): + self.original_message = original + self.__getattribute__ = self.original_message.__getattribute__ + self.__str__ = self.original_message.__str__ + self.__repr__ = self.original_message.__repr__ + self.stringify = self.original_message.stringify + self.to_dict = self.original_message.to_dict + self._client = client + self._text = None + self._reply_to = None + self._buttons = None + self._buttons_flat = [] + self.from_user = entities[self.original_message.from_id] + self.chat = entities[get_peer_id(self.original_message.to_id)] + + def __getattribute__(self, item): + return getattr(self.original_message, item) + + @property + def client(self): + return self._client + + @property + def text(self): + """ + The message text, markdown-formatted. + """ + if self._text is None: + if not self.original_message.entities: + return self.original_message.message + self._text = markdown.unparse(self.original_message.message, + self.original_message.entities or []) + return self._text + + @property + def raw_text(self): + """ + The raw message text, ignoring any formatting. + """ + return self.original_message.message + + @property + def buttons(self): + """ + Returns a matrix (list of lists) containing all buttons of the message + as `telethon.tl.custom.messagebutton.MessageButton` instances. + """ + if self._buttons is None and self.original_message.reply_markup: + if isinstance(self.original_message.reply_markup, ( + types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): + self._buttons = [[ + MessageButton(self._client, button, self.from_user, + self.chat, self.original_message.id) + for button in row.buttons + ] for row in self.original_message.reply_markup.rows] + self._buttons_flat = [x for row in self._buttons for x in row] + return self._buttons + + @property + def button_count(self): + """ + Returns the total button count. + """ + return len(self._buttons_flat) if self.buttons else 0 + + @property + def reply_to(self): + """ + The :tl:`Message` that this message is replying to, or ``None``. + + Note that this will make a network call to fetch the message and + will later be cached. + """ + if self._reply_to is None: + if not self.original_message.reply_to_msg_id: + return None + self._reply_to = self._client.get_messages( + self.original_message.to_id, + ids=self.original_message.reply_to_msg_id + ) + + def reply(self, *args, **kwargs): + """ + Replies to the message (as a reply). Shorthand for + `telethon.telegram_client.TelegramClient.send_message` with + both ``entity`` and ``reply_to`` already set. + """ + kwargs['reply_to'] = self.original_message.id + return self._client.send_message(self.original_message.to_id, + *args, **kwargs) + + def download_media(self, *args, **kwargs): + """ + Downloads the media contained in the message, if any. + `telethon.telegram_client.TelegramClient.download_media` with + the ``message`` already set. + """ + return self._client.download_media(self.original_message, + *args, **kwargs) + + def get_entities_text(self): + """ + Returns a list of tuples [(:tl:`MessageEntity`, `str`)], the string + being the inner text of the message entity (like bold, italics, etc). + """ + texts = markdown.get_inner_text(self.original_message.message, + self.original_message.entities) + return list(zip(self.original_message.entities, texts)) + + def click(self, i=None, j=None, *, text=None, filter=None): + """ + Clicks the inline keyboard button of the message, if any. + + If the message has a non-inline keyboard, clicking it will + send the message, switch to inline, or open its URL. + + Args: + i (`int`): + Clicks the i'th button (starting from the index 0). + Will ``raise IndexError`` if out of bounds. Example: + + >>> message = Message(...) + >>> # Clicking the 3rd button + >>> # [button1] [button2] + >>> # [ button3 ] + >>> # [button4] [button5] + >>> message.click(2) # index + + j (`int`): + Clicks the button at position (i, j), these being the + indices for the (row, column) respectively. Example: + + >>> # Clicking the 2nd button on the 1st row. + >>> # [button1] [button2] + >>> # [ button3 ] + >>> # [button4] [button5] + >>> message.click(0, 1) # (row, column) + + This is equivalent to ``message.buttons[0][1].click()``. + + text (`str` | `callable`): + Clicks the first button with the text "text". This may + also be a callable, like a ``re.compile(...).match``, + and the text will be passed to it. + + filter (`callable`): + Clicks the first button for which the callable + returns ``True``. The callable should accept a single + `telethon.tl.custom.messagebutton.MessageButton` argument. + """ + if sum(int(x is not None) for x in (i, text, filter)) >= 2: + raise ValueError('You can only set either of i, text or filter') + + if text is not None: + if callable(text): + for button in self._buttons_flat: + if text(button.text): + return button.click() + else: + for button in self._buttons_flat: + if button.text == text: + return button.click() + return + + if filter is not None: + for button in self._buttons_flat: + if filter(button): + return button.click() + return + + if i is None: + i = 0 + if j is None: + return self._buttons_flat[i].click() + else: + return self._buttons[i][j].click() diff --git a/telethon/tl/custom/messagebutton.py b/telethon/tl/custom/messagebutton.py new file mode 100644 index 00000000..edbe96f2 --- /dev/null +++ b/telethon/tl/custom/messagebutton.py @@ -0,0 +1,69 @@ +from .. import types, functions +import webbrowser + + +class MessageButton: + """ + Custom class that encapsulates a message providing an abstraction to + easily access some commonly needed features (such as the markdown text + or the text for a given message entity). + + Attributes: + + button (:tl:`KeyboardButton`): + The original :tl:`KeyboardButton` object. + """ + def __init__(self, client, original, from_user, chat, msg_id): + self.button = original + self._from = from_user + self._chat = chat + self._msg_id = msg_id + self._client = client + + @property + def client(self): + return self._client + + @property + def text(self): + """The text string of the button.""" + return self.button.text + + @property + def data(self): + """The ``bytes`` data for :tl:`KeyboardButtonCallback` objects.""" + if isinstance(self.button, types.KeyboardButtonCallback): + return self.button.data + + @property + def inline_query(self): + """The query ``str`` for :tl:`KeyboardButtonSwitchInline` objects.""" + if isinstance(self.button, types.KeyboardButtonSwitchInline): + return self.button.query + + @property + def url(self): + """The url ``str`` for :tl:`KeyboardButtonUrl` objects.""" + if isinstance(self.button, types.KeyboardButtonUrl): + return self.button.url + + def click(self): + """ + Clicks the inline keyboard button of the message, if any. + + If the message has a non-inline keyboard, clicking it will + send the message, switch to inline, or open its URL. + """ + if isinstance(self.button, types.KeyboardButton): + return self._client.send_message( + self._chat, self.button.text, reply_to=self._msg_id) + elif isinstance(self.button, types.KeyboardButtonCallback): + return self._client(functions.messages.GetBotCallbackAnswerRequest( + peer=self._chat, msg_id=self._msg_id, data=self.button.data + ), retries=1) + elif isinstance(self.button, types.KeyboardButtonSwitchInline): + return self._client(functions.messages.StartBotRequest( + bot=self._from, peer=self._chat, start_param=self.button.query + ), retries=1) + elif isinstance(self.button, types.KeyboardButtonUrl): + return webbrowser.open(self.button.url) From 192b7af1369ce03255e7d222058de0be98833177 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 12:24:25 +0200 Subject: [PATCH 02/15] Lazily load user/input user on Message --- telethon/tl/custom/message.py | 50 ++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index d36e4f0d..fc9af952 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -1,6 +1,6 @@ from .. import types from ...extensions import markdown -from ...utils import get_peer_id +from ...utils import get_input_peer, get_peer_id from .messagebutton import MessageButton @@ -19,7 +19,7 @@ class Message: Attributes not described here are the same as those available in the original :tl:`Message`. """ - def __init__(self, client, original, entities=None): + def __init__(self, client, original, entities, input_chat): self.original_message = original self.__getattribute__ = self.original_message.__getattribute__ self.__str__ = self.original_message.__str__ @@ -31,8 +31,10 @@ class Message: self._reply_to = None self._buttons = None self._buttons_flat = [] - self.from_user = entities[self.original_message.from_id] - self.chat = entities[get_peer_id(self.original_message.to_id)] + self._from_user = entities.get(self.original_message.from_id) + self._chat = entities.get(get_peer_id(self.original_message.to_id)) + self._from_input_user = None + self._input_chat = input_chat def __getattribute__(self, item): return getattr(self.original_message, item) @@ -60,6 +62,46 @@ class Message: """ return self.original_message.message + @property + def from_user(self): + if self._from_user is None: + self._from_user = self._client.get_entity(self.from_input_user) + return self._from_user + + @property + def chat(self): + if self._chat is None: + self._chat = self._client.get_entity(self.input_chat) + return self._chat + + @property + def from_input_user(self): + if self._from_input_user is None: + if self._from_user is not None: + self._from_input_user = get_input_peer(self._from_user) + else: + self._from_input_user = self._client.get_input_entity( + self.original_message.from_id) + return self._from_input_user + + @property + def input_chat(self): + if self._input_chat is None: + if self._chat is not None: + self._chat = get_input_peer(self._chat) + else: + self._chat = self._client.get_input_entity( + self.original_message.to_id) + return self._input_chat + + @property + def user_id(self): + return self.original_message.from_id + + @property + def chat_id(self): + return get_peer_id(self.original_message.to_id) + @property def buttons(self): """ From 5aed494aac6521d7d2603dba199744df7d8ccbd1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 12:50:08 +0200 Subject: [PATCH 03/15] Fix custom.Message special methods --- telethon/tl/custom/message.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index fc9af952..a9b2deda 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -21,9 +21,6 @@ class Message: """ def __init__(self, client, original, entities, input_chat): self.original_message = original - self.__getattribute__ = self.original_message.__getattribute__ - self.__str__ = self.original_message.__str__ - self.__repr__ = self.original_message.__repr__ self.stringify = self.original_message.stringify self.to_dict = self.original_message.to_dict self._client = client @@ -36,9 +33,15 @@ class Message: self._from_input_user = None self._input_chat = input_chat - def __getattribute__(self, item): + def __getattr__(self, item): return getattr(self.original_message, item) + def __str__(self): + return str(self.original_message) + + def __repr__(self): + return repr(self.original_message) + @property def client(self): return self._client From b241d80958662fe635defe1906a6b25a88f1809a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 12:52:03 +0200 Subject: [PATCH 04/15] Return custom.Message from the TelegramClient --- telethon/telegram_client.py | 95 +++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ef650b3e..38f45d40 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -89,10 +89,11 @@ from .tl.types import ( MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize, PhotoSizeEmpty, MessageService, ChatParticipants, User, WebPage, ChannelParticipantsBanned, ChannelParticipantsKicked, - InputMessagesFilterEmpty + InputMessagesFilterEmpty, UpdatesCombined ) from .tl.types.messages import DialogsSlice from .tl.types.account import PasswordInputSettings, NoPassword +from .tl import custom from .extensions import markdown, html __log__ = logging.getLogger(__name__) @@ -599,9 +600,10 @@ class TelegramClient(TelegramBareClient): if _total: _total[0] = getattr(r, 'count', len(r.dialogs)) - messages = {m.id: m for m in r.messages} entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} + messages = {m.id: custom.Message(self, m, entities, None) + for m in r.messages} # Happens when there are pinned dialogs if len(r.dialogs) > limit: @@ -652,8 +654,7 @@ class TelegramClient(TelegramBareClient): """ return list(self.iter_drafts()) - @staticmethod - def _get_response_message(request, result): + def _get_response_message(self, request, result, input_chat): """ Extracts the response message known a request and Update result. The request may also be the ID of the message to match. @@ -672,26 +673,36 @@ class TelegramClient(TelegramBareClient): if isinstance(result, UpdateShort): updates = [result.update] - elif isinstance(result, Updates): + entities = {} + elif isinstance(result, (Updates, UpdatesCombined)): updates = result.updates + entities = {utils.get_peer_id(x): x + for x in itertools.chain(result.users, result.chats)} else: return + found = None for update in updates: if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)): if update.message.id == msg_id: - return update.message + found = update.message + break elif (isinstance(update, UpdateEditMessage) and not isinstance(request.peer, InputPeerChannel)): if request.id == update.message.id: - return update.message + found = update.message + break elif (isinstance(update, UpdateEditChannelMessage) and utils.get_peer_id(request.peer) == utils.get_peer_id(update.message.to_id)): if request.id == update.message.id: - return update.message + found = update.message + break + + if found: + return custom.Message(self, found, entities, input_chat) def _parse_message_text(self, message, parse_mode): """ @@ -839,17 +850,18 @@ class TelegramClient(TelegramBareClient): result = self(request) if isinstance(result, UpdateShortSentMessage): - return Message( + to_id, cls = utils.resolve_id(utils.get_peer_id(entity)) + return custom.Message(self, Message( id=result.id, - to_id=entity, + to_id=cls(to_id), message=message, date=result.date, out=result.out, media=result.media, entities=result.entities - ) + ), {}, input_chat=entity) - return self._get_response_message(request, result) + return self._get_response_message(request, result, entity) def forward_messages(self, entity, messages, from_peer=None): """ @@ -895,13 +907,20 @@ class TelegramClient(TelegramBareClient): to_peer=entity ) result = self(req) + if isinstance(result, (Updates, UpdatesCombined)): + entities = {utils.get_peer_id(x): x + for x in itertools.chain(result.users, result.chats)} + else: + entities = {} + random_to_id = {} id_to_message = {} for update in result.updates: if isinstance(update, UpdateMessageID): random_to_id[update.random_id] = update.id elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)): - id_to_message[update.message.id] = update.message + id_to_message[update.message.id] = custom.Message( + self, update.message, entities, input_chat=entity) result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id] return result[0] if single else result @@ -962,16 +981,16 @@ class TelegramClient(TelegramBareClient): message = entity entity = entity.to_id + entity = self.get_input_entity(entity) text, msg_entities = self._parse_message_text(text, parse_mode) request = EditMessageRequest( - peer=self.get_input_entity(entity), + peer=entity, id=self._get_message_id(message), message=text, no_webpage=not link_preview, entities=msg_entities ) - result = self(request) - return self._get_response_message(request, result) + return self._get_response_message(request, self(request), entity) def delete_messages(self, entity, message_ids, revoke=True): """ @@ -1189,8 +1208,7 @@ class TelegramClient(TelegramBareClient): # IDs are returned in descending order. last_id = message.id - self._make_message_friendly(message, entities) - yield message + yield custom.Message(self, message, entities, entity) have += 1 if len(r.messages) < request.limit: @@ -1204,34 +1222,6 @@ class TelegramClient(TelegramBareClient): time.sleep(max(wait_time - (time.time() - start), 0)) - @staticmethod - def _make_message_friendly(message, entities): - """ - Add a few extra attributes to the :tl:`Message` to be friendlier. - - To make messages more friendly, always add message - to service messages, and action to normal messages. - """ - # TODO Create an actual friendlier class - message.message = getattr(message, 'message', None) - message.action = getattr(message, 'action', None) - message.to = entities[utils.get_peer_id(message.to_id)] - message.sender = ( - None if not message.from_id else - entities[utils.get_peer_id(message.from_id)] - ) - if getattr(message, 'fwd_from', None): - message.fwd_from.sender = ( - None if not message.fwd_from.from_id else - entities[utils.get_peer_id(message.fwd_from.from_id)] - ) - message.fwd_from.channel = ( - None if not message.fwd_from.channel_id else - entities[utils.get_peer_id( - PeerChannel(message.fwd_from.channel_id) - )] - ) - def _iter_ids(self, entity, ids, total): """ Special case for `iter_messages` when it should only fetch some IDs. @@ -1253,8 +1243,7 @@ class TelegramClient(TelegramBareClient): if isinstance(message, MessageEmpty): yield None else: - self._make_message_friendly(message, entities) - yield message + yield custom.Message(self, message, entities, entity) def get_messages(self, *args, **kwargs): """ @@ -1355,6 +1344,9 @@ class TelegramClient(TelegramBareClient): if isinstance(message, int): return message + if isinstance(message, custom.Message): + return message.original_message.id + try: if message.SUBCLASS_OF_ID == 0x790009e3: # hex(crc32(b'Message')) = 0x790009e3 @@ -1676,7 +1668,8 @@ class TelegramClient(TelegramBareClient): reply_to_msg_id=reply_to, message=caption, entities=msg_entities) - return self._get_response_message(request, self(request)) + return self._get_response_message(request, self(request), + entity) as_image = utils.is_image(file) and not force_document use_cache = InputPhoto if as_image else InputDocument @@ -1774,7 +1767,7 @@ class TelegramClient(TelegramBareClient): # send the media message to the desired entity. request = SendMediaRequest(entity, media, reply_to_msg_id=reply_to, message=caption, entities=msg_entities) - msg = self._get_response_message(request, self(request)) + msg = self._get_response_message(request, self(request), entity) if msg and isinstance(file_handle, InputSizedFile): # There was a response message and we didn't use cached # version, so cache whatever we just sent to the database. @@ -1840,7 +1833,7 @@ class TelegramClient(TelegramBareClient): entity, reply_to_msg_id=reply_to, multi_media=media )) return [ - self._get_response_message(update.id, result) + self._get_response_message(update.id, result, entity) for update in result.updates if isinstance(update, UpdateMessageID) ] From 9e4854fcce512013adf7e6035700cb223b8b8ec1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 13:30:22 +0200 Subject: [PATCH 05/15] Use custom.Message in events --- telethon/events/chataction.py | 8 +++++++- telethon/events/common.py | 6 ++++++ telethon/events/messageread.py | 11 ++--------- telethon/events/newmessage.py | 7 ++++++- telethon/telegram_client.py | 2 +- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index cbacba82..c6fcb374 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -1,6 +1,6 @@ from .common import EventBuilder, EventCommon, name_inner_event from .. import utils -from ..tl import types, functions +from ..tl import types, functions, custom @name_inner_event @@ -158,6 +158,12 @@ class ChatAction(EventBuilder): self.new_title = new_title self.unpin = unpin + def _set_client(self, client): + super()._set_client(client) + if self.action_message: + self.action_message = custom.Message( + client, self.action_message, self._entities, None) + def respond(self, *args, **kwargs): """ Responds to the chat action message (not as a reply). Shorthand for diff --git a/telethon/events/common.py b/telethon/events/common.py index a7c5db91..e5ecd60f 100644 --- a/telethon/events/common.py +++ b/telethon/events/common.py @@ -103,6 +103,12 @@ class EventCommon(abc.ABC): ) self.is_channel = isinstance(chat_peer, types.PeerChannel) + def _set_client(self, client): + """ + Setter so subclasses can act accordingly when the client is set. + """ + self._client = client + def _get_entity(self, msg_id, entity_id, chat=None): """ Helper function to call :tl:`GetMessages` on the give msg_id and diff --git a/telethon/events/messageread.py b/telethon/events/messageread.py index 16db40bb..3edd8a7f 100644 --- a/telethon/events/messageread.py +++ b/telethon/events/messageread.py @@ -100,16 +100,9 @@ class MessageRead(EventBuilder): chat = self.input_chat if not chat: self._messages = [] - elif isinstance(chat, types.InputPeerChannel): - self._messages =\ - self._client(functions.channels.GetMessagesRequest( - chat, self._message_ids - )).messages else: - self._messages =\ - self._client(functions.messages.GetMessagesRequest( - self._message_ids - )).messages + self._messages = self._client.get_messages( + chat, ids=self._message_ids) return self._messages diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index 082c9737..09b27e7b 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -3,7 +3,7 @@ import re from .common import EventBuilder, EventCommon, name_inner_event from .. import utils from ..extensions import markdown -from ..tl import types, functions +from ..tl import types, functions, custom @name_inner_event @@ -154,6 +154,11 @@ class NewMessage(EventBuilder): self.is_reply = bool(message.reply_to_msg_id) self._reply_message = None + def _set_client(self, client): + super()._set_client(client) + self.message = custom.Message( + client, self.message, self._entities, None) + def respond(self, *args, **kwargs): """ Responds to the message (not as a reply). Shorthand for diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 38f45d40..2378c104 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2416,7 +2416,7 @@ class TelegramClient(TelegramBareClient): for builder, callback in self._event_builders: event = builder.build(update) if event: - event._client = self + event._set_client(self) event.original_update = update try: callback(event) From 58f621ba8230b9e45861ee87d0afebd46a339e17 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 13:50:08 +0200 Subject: [PATCH 06/15] Make custom.Message more consistent with previous patches --- telethon/tl/custom/message.py | 53 ++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index a9b2deda..d806d7cc 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -28,9 +28,9 @@ class Message: self._reply_to = None self._buttons = None self._buttons_flat = [] - self._from_user = entities.get(self.original_message.from_id) + self._sender = entities.get(self.original_message.from_id) self._chat = entities.get(get_peer_id(self.original_message.to_id)) - self._from_input_user = None + self._input_sender = None self._input_chat = input_chat def __getattr__(self, item): @@ -50,8 +50,10 @@ class Message: def text(self): """ The message text, markdown-formatted. + Will be ``None`` for :tl:`MessageService`. """ - if self._text is None: + if self._text is None\ + and isinstance(self.original_message, types.Message): if not self.original_message.entities: return self.original_message.message self._text = markdown.unparse(self.original_message.message, @@ -62,14 +64,33 @@ class Message: def raw_text(self): """ The raw message text, ignoring any formatting. + Will be ``None`` for :tl:`MessageService`. """ - return self.original_message.message + if isinstance(self.original_message, types.Message): + return self.original_message.message @property - def from_user(self): - if self._from_user is None: - self._from_user = self._client.get_entity(self.from_input_user) - return self._from_user + def message(self): + """ + The raw message text, ignoring any formatting. + Will be ``None`` for :tl:`MessageService`. + """ + return self.raw_text + + @property + def action(self): + """ + The :tl:`MessageAction` for the :tl:`MessageService`. + Will be ``None`` for :tl:`Message`. + """ + if isinstance(self.original_message, types.MessageService): + return self.original_message.action + + @property + def sender(self): + if self._sender is None: + self._sender = self._client.get_entity(self.input_sender) + return self._sender @property def chat(self): @@ -78,14 +99,14 @@ class Message: return self._chat @property - def from_input_user(self): - if self._from_input_user is None: - if self._from_user is not None: - self._from_input_user = get_input_peer(self._from_user) + def input_sender(self): + if self._input_sender is None: + if self._sender is not None: + self._input_sender = get_input_peer(self._sender) else: - self._from_input_user = self._client.get_input_entity( + self._input_sender = self._client.get_input_entity( self.original_message.from_id) - return self._from_input_user + return self._input_sender @property def input_chat(self): @@ -115,8 +136,8 @@ class Message: if isinstance(self.original_message.reply_markup, ( types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): self._buttons = [[ - MessageButton(self._client, button, self.from_user, - self.chat, self.original_message.id) + MessageButton(self._client, button, self.input_sender, + self.input_chat, self.original_message.id) for button in row.buttons ] for row in self.original_message.reply_markup.rows] self._buttons_flat = [x for row in self._buttons for x in row] From 66d5443fcdb9be40fdab5873eb3826a6227cc4c4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 13:56:33 +0200 Subject: [PATCH 07/15] Add custom.Message.fwd_from_entity --- telethon/tl/custom/message.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index d806d7cc..932da8d1 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -32,6 +32,14 @@ class Message: self._chat = entities.get(get_peer_id(self.original_message.to_id)) self._input_sender = None self._input_chat = input_chat + self._fwd_from_entity = None + if getattr(self.original_message, 'fwd_from', None): + fwd = self.original_message.fwd_from + if fwd.from_id: + self._fwd_from_entity = entities.get(fwd.from_id) + elif fwd.channel_id: + self._fwd_from_entity = entities.get(get_peer_id( + types.PeerChannel(fwd.channel_id))) def __getattr__(self, item): return getattr(self.original_message, item) @@ -166,6 +174,23 @@ class Message: ids=self.original_message.reply_to_msg_id ) + @property + def fwd_from_entity(self): + """ + If the :tl:`Message` is a forwarded message, returns the :tl:`User` + or :tl:`Channel` who originally sent the message, or ``None``. + """ + if self._fwd_from_entity is None: + if getattr(self.original_message, 'fwd_from', None): + fwd = self.original_message.fwd_from + if fwd.from_id: + self._fwd_from_entity = self._client.get_entity( + fwd.from_id) + elif fwd.channel_id: + self._fwd_from_entity = self._client.get_entity( + get_peer_id(types.PeerChannel(fwd.channel_id))) + return self._fwd_from_entity + def reply(self, *args, **kwargs): """ Replies to the message (as a reply). Shorthand for From a1c511429e84189d70ce05993b6245c65187dcb0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 14:01:42 +0200 Subject: [PATCH 08/15] Port NewMessage.edit/delete to custom.Message --- telethon/tl/custom/message.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 932da8d1..92bdcc77 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -191,6 +191,8 @@ class Message: get_peer_id(types.PeerChannel(fwd.channel_id))) return self._fwd_from_entity + # TODO events.NewMessage and this class share a lot of code; merge them? + # Can we consider the event of a new message to be a message in itself? def reply(self, *args, **kwargs): """ Replies to the message (as a reply). Shorthand for @@ -201,6 +203,38 @@ class Message: return self._client.send_message(self.original_message.to_id, *args, **kwargs) + def edit(self, *args, **kwargs): + """ + Edits the message iff it's outgoing. Shorthand for + `telethon.telegram_client.TelegramClient.edit_message` with + both ``entity`` and ``message`` already set. + + Returns ``None`` if the message was incoming, or the edited + :tl:`Message` otherwise. + """ + if self.original_message.fwd_from: + return None + if not self.original_message.out: + if not isinstance(self.original_message.to_id, types.PeerUser): + return None + me = self._client.get_me(input_peer=True) + if self.original_message.to_id.user_id != me.user_id: + return None + + return self._client.edit_message( + self.input_chat, self.original_message, *args, **kwargs) + + def delete(self, *args, **kwargs): + """ + Deletes the message. You're responsible for checking whether you + have the permission to do so, or to except the error otherwise. + Shorthand for + `telethon.telegram_client.TelegramClient.delete_messages` with + ``entity`` and ``message_ids`` already set. + """ + return self._client.delete_messages( + self.input_chat, [self.original_message], *args, **kwargs) + def download_media(self, *args, **kwargs): """ Downloads the media contained in the message, if any. From 2191fbf30b5e23b2a3be3294a6e9565a2499176c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 14:09:43 +0200 Subject: [PATCH 09/15] Fix custom.Message.click not having buttons --- telethon/tl/custom/message.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 92bdcc77..6def2c0d 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -27,7 +27,7 @@ class Message: self._text = None self._reply_to = None self._buttons = None - self._buttons_flat = [] + self._buttons_flat = None self._sender = entities.get(self.original_message.from_id) self._chat = entities.get(get_peer_id(self.original_message.to_id)) self._input_sender = None @@ -260,6 +260,8 @@ class Message: If the message has a non-inline keyboard, clicking it will send the message, switch to inline, or open its URL. + Does nothing if the message has no buttons. + Args: i (`int`): Clicks the i'th button (starting from the index 0). @@ -297,6 +299,9 @@ class Message: if sum(int(x is not None) for x in (i, text, filter)) >= 2: raise ValueError('You can only set either of i, text or filter') + if not self.buttons: + return # Accessing the property sets self._buttons[_flat] + if text is not None: if callable(text): for button in self._buttons_flat: From 9db9d1ed5c553c253b9e864435299f715c1d067a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 31 May 2018 22:39:32 +0200 Subject: [PATCH 10/15] Implement __bytes__ and use count instead sum --- telethon/tl/custom/message.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 6def2c0d..28b7cdb8 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -50,6 +50,9 @@ class Message: def __repr__(self): return repr(self.original_message) + def __bytes__(self): + return bytes(self.original_message) + @property def client(self): return self._client @@ -296,7 +299,7 @@ class Message: returns ``True``. The callable should accept a single `telethon.tl.custom.messagebutton.MessageButton` argument. """ - if sum(int(x is not None) for x in (i, text, filter)) >= 2: + if (i, text, filter).count(None) >= 2: raise ValueError('You can only set either of i, text or filter') if not self.buttons: From e2ce55871ed2020aba975f0e04b3e53d31cf5d4a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 1 Jun 2018 21:20:34 +0200 Subject: [PATCH 11/15] Replace custom.Message's class on creation --- telethon/tl/custom/message.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 28b7cdb8..7088784e 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -41,6 +41,14 @@ class Message: self._fwd_from_entity = entities.get(get_peer_id( types.PeerChannel(fwd.channel_id))) + def __new__(cls, client, original, entities, input_chat): + if isinstance(original, types.Message): + return super().__new__(_CustomMessage) + elif isinstance(original, types.MessageService): + return super().__new__(_CustomMessageService) + else: + return cls + def __getattr__(self, item): return getattr(self.original_message, item) @@ -328,3 +336,11 @@ class Message: return self._buttons_flat[i].click() else: return self._buttons[i][j].click() + + +class _CustomMessage(Message, types.Message): + pass + + +class _CustomMessageService(Message, types.MessageService): + pass From 97b0a0610e65a1557f5ae9e2c4db490833dfe9c2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 2 Jun 2018 12:09:21 +0200 Subject: [PATCH 12/15] Support get_messages(ids=) without entity --- telethon/telegram_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 2378c104..c6f55675 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1123,7 +1123,11 @@ class TelegramClient(TelegramBareClient): an higher limit, so you're free to set the ``batch_size`` that you think may be good. """ - entity = self.get_input_entity(entity) + # It's possible to get messages by ID without their entity, so only + # fetch the input version if we're not using IDs or if it was given. + if not ids or entity: + entity = self.get_input_entity(entity) + if ids: if not utils.is_list_like(ids): ids = (ids,) From 6dcd0911a78e49ad7bd87f9df1097740de24a904 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 2 Jun 2018 12:30:25 +0200 Subject: [PATCH 13/15] Move events.NewMessage properties to custom.Message --- telethon/events/newmessage.py | 301 ++-------------------------------- telethon/tl/custom/message.py | 238 +++++++++++++++++++++++++-- 2 files changed, 233 insertions(+), 306 deletions(-) diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index 09b27e7b..8c1f6625 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -1,9 +1,7 @@ import re from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..extensions import markdown -from ..tl import types, functions, custom +from ..tl import types, custom @name_inner_event @@ -116,23 +114,19 @@ class NewMessage(EventBuilder): class Event(EventCommon): """ - Represents the event of a new message. + Represents the event of a new message. This event can be treated + to all effects as a `telethon.tl.custom.message.Message`, so please + **refer to its documentation** to know what you can do with this event. Members: message (:tl:`Message`): - This is the original :tl:`Message` object. + This is the only difference with the received + `telethon.tl.custom.message.Message`, and will + return the `telethon.tl.custom.message.Message` itself, + not the text. - is_private (`bool`): - True if the message was sent as a private message. - - is_group (`bool`): - True if the message was sent on a group or megagroup. - - is_channel (`bool`): - True if the message was sent on a megagroup or channel. - - is_reply (`str`): - Whether the message is a reply to some other or not. + See `telethon.tl.custom.message.Message` for the rest of + available members and methods. """ def __init__(self, message): if not message.out and isinstance(message.to_id, types.PeerUser): @@ -146,282 +140,11 @@ class NewMessage(EventBuilder): msg_id=message.id, broadcast=bool(message.post)) self.message = message - self._text = None - - self._input_sender = None - self._sender = None - - self.is_reply = bool(message.reply_to_msg_id) - self._reply_message = None def _set_client(self, client): super()._set_client(client) self.message = custom.Message( client, self.message, self._entities, None) - def respond(self, *args, **kwargs): - """ - Responds to the message (not as a reply). Shorthand for - `telethon.telegram_client.TelegramClient.send_message` with - ``entity`` already set. - """ - return self._client.send_message(self.input_chat, *args, **kwargs) - - def reply(self, *args, **kwargs): - """ - Replies to the message (as a reply). Shorthand for - `telethon.telegram_client.TelegramClient.send_message` with - both ``entity`` and ``reply_to`` already set. - """ - kwargs['reply_to'] = self.message.id - return self._client.send_message(self.input_chat, *args, **kwargs) - - def forward_to(self, *args, **kwargs): - """ - Forwards the message. Shorthand for - `telethon.telegram_client.TelegramClient.forward_messages` with - both ``messages`` and ``from_peer`` already set. - """ - kwargs['messages'] = self.message.id - kwargs['from_peer'] = self.input_chat - return self._client.forward_messages(*args, **kwargs) - - def edit(self, *args, **kwargs): - """ - Edits the message iff it's outgoing. Shorthand for - `telethon.telegram_client.TelegramClient.edit_message` with - both ``entity`` and ``message`` already set. - - Returns ``None`` if the message was incoming, or the edited - :tl:`Message` otherwise. - """ - if self.message.fwd_from: - return None - if not self.message.out: - if not isinstance(self.message.to_id, types.PeerUser): - return None - me = self._client.get_me(input_peer=True) - if self.message.to_id.user_id != me.user_id: - return None - - return self._client.edit_message(self.input_chat, - self.message, - *args, **kwargs) - - def delete(self, *args, **kwargs): - """ - Deletes the message. You're responsible for checking whether you - have the permission to do so, or to except the error otherwise. - Shorthand for - `telethon.telegram_client.TelegramClient.delete_messages` with - ``entity`` and ``message_ids`` already set. - """ - return self._client.delete_messages(self.input_chat, - [self.message], - *args, **kwargs) - - @property - def input_sender(self): - """ - This (:tl:`InputPeer`) is the input version of the user who - sent the message. Similarly to ``input_chat``, this doesn't have - things like username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat, or if the message a broadcast on a channel. - """ - if self._input_sender is None: - if self.is_channel and not self.is_group: - return None - - try: - self._input_sender = self._client.get_input_entity( - self.message.from_id - ) - except (ValueError, TypeError): - # We can rely on self.input_chat for this - self._sender, self._input_sender = self._get_entity( - self.message.id, - self.message.from_id, - chat=self.input_chat - ) - - return self._input_sender - - @property - def sender(self): - """ - This (:tl:`User`) may make an API call the first time to get - the most up to date version of the sender (mostly when the event - doesn't belong to a channel), so keep that in mind. - - ``input_sender`` needs to be available (often the case). - """ - if not self.input_sender: - return None - - if self._sender is None: - self._sender = \ - self._entities.get(utils.get_peer_id(self._input_sender)) - - if self._sender is None: - self._sender = self._client.get_entity(self._input_sender) - - return self._sender - - @property - def sender_id(self): - """ - Returns the marked sender integer ID, if present. - """ - return self.message.from_id - - @property - def text(self): - """ - The message text, markdown-formatted. - """ - if self._text is None: - if not self.message.entities: - return self.message.message - self._text = markdown.unparse(self.message.message, - self.message.entities or []) - return self._text - - @property - def raw_text(self): - """ - The raw message text, ignoring any formatting. - """ - return self.message.message - - @property - def reply_message(self): - """ - This optional :tl:`Message` will make an API call the first - time to get the full :tl:`Message` object that one was replying to, - so use with care as there is no caching besides local caching yet. - """ - if not self.message.reply_to_msg_id: - return None - - if self._reply_message is None: - if isinstance(self.input_chat, types.InputPeerChannel): - r = self._client(functions.channels.GetMessagesRequest( - self.input_chat, [self.message.reply_to_msg_id] - )) - else: - r = self._client(functions.messages.GetMessagesRequest( - [self.message.reply_to_msg_id] - )) - if not isinstance(r, types.messages.MessagesNotModified): - self._reply_message = r.messages[0] - - return self._reply_message - - @property - def forward(self): - """ - The unmodified :tl:`MessageFwdHeader`, if present.. - """ - return self.message.fwd_from - - @property - def media(self): - """ - The unmodified :tl:`MessageMedia`, if present. - """ - return self.message.media - - @property - def photo(self): - """ - If the message media is a photo, - this returns the :tl:`Photo` object. - """ - if isinstance(self.message.media, types.MessageMediaPhoto): - photo = self.message.media.photo - if isinstance(photo, types.Photo): - return photo - - @property - def document(self): - """ - If the message media is a document, - this returns the :tl:`Document` object. - """ - if isinstance(self.message.media, types.MessageMediaDocument): - doc = self.message.media.document - if isinstance(doc, types.Document): - return doc - - def _document_by_attribute(self, kind, condition=None): - """ - Helper method to return the document only if it has an attribute - that's an instance of the given kind, and passes the condition. - """ - doc = self.document - if doc: - for attr in doc.attributes: - if isinstance(attr, kind): - if not condition or condition(doc): - return doc - - @property - def audio(self): - """ - If the message media is a document with an Audio attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeAudio, - lambda attr: not attr.voice) - - @property - def voice(self): - """ - If the message media is a document with a Voice attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeAudio, - lambda attr: attr.voice) - - @property - def video(self): - """ - If the message media is a document with a Video attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeVideo) - - @property - def video_note(self): - """ - If the message media is a document with a Video attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeVideo, - lambda attr: attr.round_message) - - @property - def gif(self): - """ - If the message media is a document with an Animated attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeAnimated) - - @property - def sticker(self): - """ - If the message media is a document with a Sticker attribute, - this returns the :tl:`Document` object. - """ - return self._document_by_attribute(types.DocumentAttributeSticker) - - @property - def out(self): - """ - Whether the message is outgoing (i.e. you sent it from - another session) or incoming (i.e. someone else sent it). - """ - return self.message.out + def __getattr__(self, item): + return getattr(self.message, item) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 7088784e..e6287962 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -25,12 +25,15 @@ class Message: self.to_dict = self.original_message.to_dict self._client = client self._text = None - self._reply_to = None + self._reply_message = None self._buttons = None self._buttons_flat = None self._sender = entities.get(self.original_message.from_id) self._chat = entities.get(get_peer_id(self.original_message.to_id)) - self._input_sender = None + if self._sender: + self._input_sender = get_input_peer(self._sender) + else: + self._input_sender = None self._input_chat = input_chat self._fwd_from_entity = None if getattr(self.original_message, 'fwd_from', None): @@ -105,46 +108,128 @@ class Message: if isinstance(self.original_message, types.MessageService): return self.original_message.action + def _reload_message(self): + """ + Re-fetches this message to reload the sender and chat entities, + along with their input versions. + """ + try: + chat = self.input_chat if self.is_channel else None + msg = self._client.get_messages(chat, ids=self.original_message.id) + except ValueError: + return # We may not have the input chat/get message failed + if not msg: + return # The message may be deleted and it will be None + + self._sender = msg._sender + self._input_sender = msg._input_sender + self._chat = msg._chat + self._input_chat = msg._input_chat + @property def sender(self): + """ + This (:tl:`User`) may make an API call the first time to get + the most up to date version of the sender (mostly when the event + doesn't belong to a channel), so keep that in mind. + + `input_sender` needs to be available (often the case). + """ if self._sender is None: - self._sender = self._client.get_entity(self.input_sender) + try: + self._sender = self._client.get_entity(self.input_sender) + except ValueError: + self._reload_message() return self._sender @property def chat(self): if self._chat is None: - self._chat = self._client.get_entity(self.input_chat) + try: + self._chat = self._client.get_entity(self.input_chat) + except ValueError: + self._reload_message() return self._chat @property def input_sender(self): + """ + This (:tl:`InputPeer`) is the input version of the user who + sent the message. Similarly to `input_chat`, this doesn't have + things like username or similar, but still useful in some cases. + + Note that this might not be available if the library can't + find the input chat, or if the message a broadcast on a channel. + """ if self._input_sender is None: + if self.is_channel and not self.is_group: + return None if self._sender is not None: self._input_sender = get_input_peer(self._sender) else: - self._input_sender = self._client.get_input_entity( - self.original_message.from_id) + try: + self._input_sender = self._client.get_input_entity( + self.original_message.from_id) + except ValueError: + self._reload_message() return self._input_sender @property def input_chat(self): if self._input_chat is None: + if self._chat is None: + try: + self._chat = self._client.get_input_entity( + self.original_message.to_id) + except ValueError: + # There's a chance that the chat is a recent new dialog. + # The input chat cannot rely on ._reload_message() because + # said method may need the input chat. + target = self.chat_id + for d in self._client.iter_dialogs(100): + if d.id == target: + self._chat = d.entity + break if self._chat is not None: - self._chat = get_input_peer(self._chat) - else: - self._chat = self._client.get_input_entity( - self.original_message.to_id) + self._input_chat = get_input_peer(self._chat) + return self._input_chat @property - def user_id(self): + def sender_id(self): + """ + Returns the marked sender integer ID, if present. + """ return self.original_message.from_id @property def chat_id(self): + """ + Returns the marked chat integer ID. + """ return get_peer_id(self.original_message.to_id) + @property + def is_private(self): + """True if the message was sent as a private message.""" + return isinstance(self.original_message.to_id, types.PeerUser) + + @property + def is_group(self): + """True if the message was sent on a group or megagroup.""" + return not self.original_message.broadcast and isinstance( + self.original_message.to_id, (types.PeerChat, types.PeerChannel)) + + @property + def is_channel(self): + """True if the message was sent on a megagroup or channel.""" + return isinstance(self.original_message.to_id, types.PeerChannel) + + @property + def is_reply(self): + """True if the message is a reply to some other or not.""" + return bool(self.original_message.reply_to_msg_id) + @property def buttons(self): """ @@ -170,21 +255,116 @@ class Message: return len(self._buttons_flat) if self.buttons else 0 @property - def reply_to(self): + def photo(self): + """ + If the message media is a photo, + this returns the :tl:`Photo` object. + """ + if isinstance(self.original_message.media, types.MessageMediaPhoto): + photo = self.original_message.media.photo + if isinstance(photo, types.Photo): + return photo + + @property + def document(self): + """ + If the message media is a document, + this returns the :tl:`Document` object. + """ + if isinstance(self.original_message.media, types.MessageMediaDocument): + doc = self.original_message.media.document + if isinstance(doc, types.Document): + return doc + + def _document_by_attribute(self, kind, condition=None): + """ + Helper method to return the document only if it has an attribute + that's an instance of the given kind, and passes the condition. + """ + doc = self.document + if doc: + for attr in doc.attributes: + if isinstance(attr, kind): + if not condition or condition(doc): + return doc + + @property + def audio(self): + """ + If the message media is a document with an Audio attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeAudio, + lambda attr: not attr.voice) + + @property + def voice(self): + """ + If the message media is a document with a Voice attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeAudio, + lambda attr: attr.voice) + + @property + def video(self): + """ + If the message media is a document with a Video attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeVideo) + + @property + def video_note(self): + """ + If the message media is a document with a Video attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeVideo, + lambda attr: attr.round_message) + + @property + def gif(self): + """ + If the message media is a document with an Animated attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeAnimated) + + @property + def sticker(self): + """ + If the message media is a document with a Sticker attribute, + this returns the :tl:`Document` object. + """ + return self._document_by_attribute(types.DocumentAttributeSticker) + + @property + def out(self): + """ + Whether the message is outgoing (i.e. you sent it from + another session) or incoming (i.e. someone else sent it). + """ + return self.original_message.out + + @property + def reply_message(self): """ The :tl:`Message` that this message is replying to, or ``None``. Note that this will make a network call to fetch the message and will later be cached. """ - if self._reply_to is None: + if self._reply_message is None: if not self.original_message.reply_to_msg_id: return None - self._reply_to = self._client.get_messages( - self.original_message.to_id, + self._reply_message = self._client.get_messages( + self.input_chat if self.is_channel else None, ids=self.original_message.reply_to_msg_id ) + return self._reply_message + @property def fwd_from_entity(self): """ @@ -202,8 +382,14 @@ class Message: get_peer_id(types.PeerChannel(fwd.channel_id))) return self._fwd_from_entity - # TODO events.NewMessage and this class share a lot of code; merge them? - # Can we consider the event of a new message to be a message in itself? + def respond(self, *args, **kwargs): + """ + Responds to the message (not as a reply). Shorthand for + `telethon.telegram_client.TelegramClient.send_message` with + ``entity`` already set. + """ + return self._client.send_message(self.input_chat, *args, **kwargs) + def reply(self, *args, **kwargs): """ Replies to the message (as a reply). Shorthand for @@ -214,6 +400,20 @@ class Message: return self._client.send_message(self.original_message.to_id, *args, **kwargs) + def forward_to(self, *args, **kwargs): + """ + Forwards the message. Shorthand for + `telethon.telegram_client.TelegramClient.forward_messages` with + both ``messages`` and ``from_peer`` already set. + + If you need to forward more than one message at once, don't use + this `forward_to` method. Use a + `telethon.telegram_client.TelegramClient` instance directly. + """ + kwargs['messages'] = self.original_message.id + kwargs['from_peer'] = self.input_chat + return self._client.forward_messages(*args, **kwargs) + def edit(self, *args, **kwargs): """ Edits the message iff it's outgoing. Shorthand for @@ -242,6 +442,10 @@ class Message: Shorthand for `telethon.telegram_client.TelegramClient.delete_messages` with ``entity`` and ``message_ids`` already set. + + If you need to delete more than one message at once, don't use + this `delete` method. Use a + `telethon.telegram_client.TelegramClient` instance directly. """ return self._client.delete_messages( self.input_chat, [self.original_message], *args, **kwargs) From 5c76af34aa81833bf7d2850330700b2a1c6d2998 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 2 Jun 2018 12:38:03 +0200 Subject: [PATCH 14/15] Fix copy-paste typo --- telethon/events/newmessage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index 8c1f6625..8bba002a 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -29,7 +29,7 @@ class NewMessage(EventBuilder): if incoming is not None and outgoing is None: outgoing = not incoming elif outgoing is not None and incoming is None: - incoming = not incoming + incoming = not outgoing if incoming and outgoing: self.incoming = self.outgoing = None # Same as no filter From f7222407deb879468523477ad364ad2422532c9b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 2 Jun 2018 12:52:38 +0200 Subject: [PATCH 15/15] Document custom.Message --- readthedocs/telethon.tl.custom.rst | 18 ++++++++++++++++++ telethon/events/chataction.py | 4 ++-- telethon/events/messageread.py | 3 ++- telethon/telegram_client.py | 19 +++++++------------ telethon/tl/custom/message.py | 16 +++++++++++++++- telethon/tl/custom/messagebutton.py | 4 ++++ 6 files changed, 48 insertions(+), 16 deletions(-) diff --git a/readthedocs/telethon.tl.custom.rst b/readthedocs/telethon.tl.custom.rst index 7f59596c..138811f6 100644 --- a/readthedocs/telethon.tl.custom.rst +++ b/readthedocs/telethon.tl.custom.rst @@ -19,3 +19,21 @@ telethon\.tl\.custom\.dialog module :members: :undoc-members: :show-inheritance: + + +telethon\.tl\.custom\.message module +------------------------------------ + +.. automodule:: telethon.tl.custom.message + :members: + :undoc-members: + :show-inheritance: + + +telethon\.tl\.custom\.messagebutton module +------------------------------------------ + +.. automodule:: telethon.tl.custom.messagebutton + :members: + :undoc-members: + :show-inheritance: diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index c6fcb374..3e653ddc 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -204,8 +204,8 @@ class ChatAction(EventBuilder): @property def pinned_message(self): """ - If ``new_pin`` is ``True``, this returns the (:tl:`Message`) - object that was pinned. + If ``new_pin`` is ``True``, this returns the + `telethon.tl.custom.message.Message` object that was pinned. """ if self._pinned_message == 0: return None diff --git a/telethon/events/messageread.py b/telethon/events/messageread.py index 3edd8a7f..b9dd6761 100644 --- a/telethon/events/messageread.py +++ b/telethon/events/messageread.py @@ -91,7 +91,8 @@ class MessageRead(EventBuilder): @property def messages(self): """ - The list of :tl:`Message` **which contents'** were read. + The list of `telethon.tl.custom.message.Message` + **which contents'** were read. Use :meth:`is_read` if you need to check whether a message was read instead checking if it's in here. diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c6f55675..395863f4 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -800,7 +800,7 @@ class TelegramClient(TelegramBareClient): Has no effect when sending a file. Returns: - The sent :tl:`Message`. + The sent `telethon.tl.custom.message.Message`. """ if file is not None: return self.send_file( @@ -880,8 +880,8 @@ class TelegramClient(TelegramBareClient): order for the forward to work. Returns: - The list of forwarded :tl:`Message`, or a single one if a list - wasn't provided as input. + The list of forwarded `telethon.tl.custom.message.Message`, + or a single one if a list wasn't provided as input. """ single = not utils.is_list_like(messages) if single: @@ -974,7 +974,7 @@ class TelegramClient(TelegramBareClient): not modified at all. Returns: - The edited :tl:`Message`. + The edited `telethon.tl.custom.message.Message`. """ if isinstance(entity, Message): text = message # Shift the parameters to the right @@ -1109,12 +1109,7 @@ class TelegramClient(TelegramBareClient): A single-item list to pass the total parameter by reference. Yields: - Instances of :tl:`Message` with extra attributes: - - * ``.sender`` = entity of the sender. - * ``.fwd_from.sender`` = if fwd_from, who sent it originally. - * ``.fwd_from.channel`` = if fwd_from, original channel. - * ``.to`` = entity to which the message was sent. + Instances of `telethon.tl.custom.message.Message`. Notes: Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to @@ -1610,8 +1605,8 @@ class TelegramClient(TelegramBareClient): it will be used to determine metadata from audio and video files. Returns: - The :tl:`Message` (or messages) containing the sent file, - or messages if a list of them was passed. + The `telethon.tl.custom.message.Message` (or messages) containing + the sent file, or messages if a list of them was passed. """ # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index e6287962..8150ab46 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -66,6 +66,10 @@ class Message: @property def client(self): + """ + Returns the `telethon.telegram_client.TelegramClient` instance that + created this instance. + """ return self._client @property @@ -176,6 +180,15 @@ class Message: @property def input_chat(self): + """ + This (:tl:`InputPeer`) is the input version of the chat where the + message was sent. Similarly to `input_sender`, this doesn't have + things like username or similar, but still useful in some cases. + + Note that this might not be available if the library doesn't know + where the message came from, and it may fetch the dialogs to try + to find it in the worst case. + """ if self._input_chat is None: if self._chat is None: try: @@ -350,7 +363,8 @@ class Message: @property def reply_message(self): """ - The :tl:`Message` that this message is replying to, or ``None``. + The `telethon.tl.custom.message.Message` that this message is replying + to, or ``None``. Note that this will make a network call to fetch the message and will later be cached. diff --git a/telethon/tl/custom/messagebutton.py b/telethon/tl/custom/messagebutton.py index edbe96f2..cd9b1ffc 100644 --- a/telethon/tl/custom/messagebutton.py +++ b/telethon/tl/custom/messagebutton.py @@ -22,6 +22,10 @@ class MessageButton: @property def client(self): + """ + Returns the `telethon.telegram_client.TelegramClient` instance that + created this instance. + """ return self._client @property