Separate parse message methods from uploads

This commit is contained in:
Lonami Exo 2018-06-10 11:30:51 +02:00
parent 83a024656c
commit 317b7053a0
4 changed files with 167 additions and 150 deletions

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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: