diff --git a/telethon/client/messageparse.py b/telethon/client/messageparse.py new file mode 100644 index 00000000..b0916cbe --- /dev/null +++ b/telethon/client/messageparse.py @@ -0,0 +1,129 @@ +import itertools +import re + +from .users import UserMethods +from .. import utils +from ..tl import types, custom + + +class MessageParseMethods(UserMethods): + + # region Public properties + + @property + def parse_mode(self): + """ + This property is the default parse mode used when sending messages. + Defaults to `telethon.extensions.markdown`. It will always + be either ``None`` or an object with ``parse`` and ``unparse`` + methods. + + When setting a different value it should be one of: + + * Object with ``parse`` and ``unparse`` methods. + * A ``callable`` to act as the parse method. + * A ``str`` indicating the ``parse_mode``. For Markdown ``'md'`` + or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'`` + may be used. + + The ``parse`` method should be a function accepting a single + parameter, the text to parse, and returning a tuple consisting + of ``(parsed message str, [MessageEntity instances])``. + + The ``unparse`` method should be the inverse of ``parse`` such + that ``assert text == unparse(*parse(text))``. + + See :tl:`MessageEntity` for allowed message entities. + """ + return self._parse_mode + + @parse_mode.setter + def parse_mode(self, mode): + self._parse_mode = utils.sanitize_parse_mode(mode) + + # endregion + + # region Private methods + + async def _parse_message_text(self, message, parse_mode): + """ + Returns a (parsed message, entities) tuple depending on ``parse_mode``. + """ + if parse_mode == utils.Default: + parse_mode = self._parse_mode + else: + parse_mode = utils.sanitize_parse_mode(parse_mode) + + if not parse_mode: + return message, [] + + message, msg_entities = parse_mode.parse(message) + for i, e in enumerate(msg_entities): + if isinstance(e, types.MessageEntityTextUrl): + m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url) + if m: + try: + msg_entities[i] = types.InputMessageEntityMentionName( + e.offset, e.length, await self.get_input_entity( + int(m.group(1)) if m.group(1) else e.url + ) + ) + except (ValueError, TypeError): + # Make no replacement + pass + + return message, msg_entities + + 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. + """ + # Telegram seems to send updateMessageID first, then updateNewMessage, + # however let's not rely on that just in case. + if isinstance(request, int): + msg_id = request + else: + msg_id = None + for update in result.updates: + if isinstance(update, types.UpdateMessageID): + if update.random_id == request.random_id: + msg_id = update.id + break + + if isinstance(result, types.UpdateShort): + updates = [result.update] + entities = {} + elif isinstance(result, (types.Updates, types.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, ( + types.UpdateNewChannelMessage, types.UpdateNewMessage)): + if update.message.id == msg_id: + found = update.message + break + + elif (isinstance(update, types.UpdateEditMessage) + and not isinstance(request.peer, types.InputPeerChannel)): + if request.id == update.message.id: + found = update.message + break + + elif (isinstance(update, types.UpdateEditChannelMessage) + and utils.get_peer_id(request.peer) == + utils.get_peer_id(update.message.to_id)): + if request.id == update.message.id: + found = update.message + break + + if found: + return custom.Message(self, found, entities, input_chat) + + # endregion diff --git a/telethon/client/messages.py b/telethon/client/messages.py index d21d81e8..2e9d9bc1 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -5,6 +5,7 @@ import time import warnings from collections import UserList +from .messageparse import MessageParseMethods from .uploads import UploadMethods from .. import utils from ..tl import types, functions, custom @@ -12,7 +13,7 @@ from ..tl import types, functions, custom __log__ = logging.getLogger(__name__) -class MessageMethods(UploadMethods): +class MessageMethods(UploadMethods, MessageParseMethods): # region Public methods diff --git a/telethon/client/uploads.py b/telethon/client/uploads.py index 3cf9ec0d..b6a162fe 100644 --- a/telethon/client/uploads.py +++ b/telethon/client/uploads.py @@ -1,16 +1,14 @@ import hashlib import io -import itertools import logging import os -import re import warnings from io import BytesIO from mimetypes import guess_type +from .messageparse import MessageParseMethods from .users import UserMethods from .. import utils, helpers -from ..extensions import markdown, html from ..tl import types, functions, custom try: @@ -23,7 +21,7 @@ except ImportError: __log__ = logging.getLogger(__name__) -class UploadMethods(UserMethods): +class UploadMethods(MessageParseMethods, UserMethods): # region Public methods @@ -356,151 +354,6 @@ class UploadMethods(UserMethods): # endregion - # region Private methods - - 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. - """ - # Telegram seems to send updateMessageID first, then updateNewMessage, - # however let's not rely on that just in case. - if isinstance(request, int): - msg_id = request - else: - msg_id = None - for update in result.updates: - if isinstance(update, types.UpdateMessageID): - if update.random_id == request.random_id: - msg_id = update.id - break - - if isinstance(result, types.UpdateShort): - updates = [result.update] - entities = {} - elif isinstance(result, (types.Updates, types.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, ( - types.UpdateNewChannelMessage, - types.UpdateNewMessage)): - if update.message.id == msg_id: - found = update.message - break - - elif (isinstance(update, types.UpdateEditMessage) and - not isinstance(request.peer, - types.InputPeerChannel)): - if request.id == update.message.id: - found = update.message - break - - elif (isinstance(update, types.UpdateEditChannelMessage) and - utils.get_peer_id(request.peer) == - utils.get_peer_id(update.message.to_id)): - if request.id == update.message.id: - found = update.message - break - - if found: - return custom.Message(self, found, entities, input_chat) - - @property - def parse_mode(self): - """ - This property is the default parse mode used when sending messages. - Defaults to `telethon.extensions.markdown`. It will always - be either ``None`` or an object with ``parse`` and ``unparse`` - methods. - - When setting a different value it should be one of: - - * Object with ``parse`` and ``unparse`` methods. - * A ``callable`` to act as the parse method. - * A ``str`` indicating the ``parse_mode``. For Markdown ``'md'`` - or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'`` - may be used. - - The ``parse`` method should be a function accepting a single - parameter, the text to parse, and returning a tuple consisting - of ``(parsed message str, [MessageEntity instances])``. - - The ``unparse`` method should be the inverse of ``parse`` such - that ``assert text == unparse(*parse(text))``. - - See :tl:`MessageEntity` for allowed message entities. - """ - return self._parse_mode - - @parse_mode.setter - def parse_mode(self, mode): - self._parse_mode = self._sanitize_parse_mode(mode) - - @staticmethod - def _sanitize_parse_mode(mode): - if not mode: - return None - - if callable(mode): - class CustomMode: - @staticmethod - def unparse(text, entities): - raise NotImplementedError - - CustomMode.parse = mode - return CustomMode - elif (all(hasattr(mode, x) for x in ('parse', 'unparse')) - and all(callable(x) for x in (mode.parse, mode.unparse))): - return mode - elif isinstance(mode, str): - try: - return { - 'md': markdown, - 'markdown': markdown, - 'htm': html, - 'html': html - }[mode.lower()] - except KeyError: - raise ValueError('Unknown parse mode {}'.format(mode)) - else: - raise TypeError('Invalid parse mode type {}'.format(mode)) - - async def _parse_message_text(self, message, parse_mode): - """ - Returns a (parsed message, entities) tuple depending on ``parse_mode``. - """ - if parse_mode == utils.Default: - parse_mode = self._parse_mode - else: - parse_mode = self._sanitize_parse_mode(parse_mode) - - if not parse_mode: - return message, [] - - message, msg_entities = parse_mode.parse(message) - for i, e in enumerate(msg_entities): - if isinstance(e, types.MessageEntityTextUrl): - m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url) - if m: - try: - msg_entities[i] = types.InputMessageEntityMentionName( - e.offset, e.length, await self.get_input_entity( - int(m.group(1)) if m.group(1) else e.url - ) - ) - except (ValueError, TypeError): - # Make no replacement - pass - - return message, msg_entities - async def _file_to_media( self, file, force_document=False, progress_callback=None, attributes=None, thumb=None, diff --git a/telethon/utils.py b/telethon/utils.py index 340574f2..4f89764f 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -12,6 +12,7 @@ import types from collections import UserList from mimetypes import guess_extension +from .extensions import markdown, html from .tl import TLObject from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, @@ -423,6 +424,39 @@ def get_message_id(message): raise TypeError('Invalid message type: {}'.format(type(message))) +def sanitize_parse_mode(mode): + """ + Converts the given parse mode into an object with + ``parse`` and ``unparse`` callable properties. + """ + if not mode: + return None + + if callable(mode): + class CustomMode: + @staticmethod + def unparse(text, entities): + raise NotImplementedError + + CustomMode.parse = mode + return CustomMode + elif (all(hasattr(mode, x) for x in ('parse', 'unparse')) + and all(callable(x) for x in (mode.parse, mode.unparse))): + return mode + elif isinstance(mode, str): + try: + return { + 'md': markdown, + 'markdown': markdown, + 'htm': html, + 'html': html + }[mode.lower()] + except KeyError: + raise ValueError('Unknown parse mode {}'.format(mode)) + else: + raise TypeError('Invalid parse mode type {}'.format(mode)) + + def get_input_location(location): """Similar to :meth:`get_input_peer`, but for input messages.""" try: