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 import warnings
from collections import UserList from collections import UserList
from .messageparse import MessageParseMethods
from .uploads import UploadMethods from .uploads import UploadMethods
from .. import utils from .. import utils
from ..tl import types, functions, custom from ..tl import types, functions, custom
@ -12,7 +13,7 @@ from ..tl import types, functions, custom
__log__ = logging.getLogger(__name__) __log__ = logging.getLogger(__name__)
class MessageMethods(UploadMethods): class MessageMethods(UploadMethods, MessageParseMethods):
# region Public methods # region Public methods

View File

@ -1,16 +1,14 @@
import hashlib import hashlib
import io import io
import itertools
import logging import logging
import os import os
import re
import warnings import warnings
from io import BytesIO from io import BytesIO
from mimetypes import guess_type from mimetypes import guess_type
from .messageparse import MessageParseMethods
from .users import UserMethods from .users import UserMethods
from .. import utils, helpers from .. import utils, helpers
from ..extensions import markdown, html
from ..tl import types, functions, custom from ..tl import types, functions, custom
try: try:
@ -23,7 +21,7 @@ except ImportError:
__log__ = logging.getLogger(__name__) __log__ = logging.getLogger(__name__)
class UploadMethods(UserMethods): class UploadMethods(MessageParseMethods, UserMethods):
# region Public methods # region Public methods
@ -356,151 +354,6 @@ class UploadMethods(UserMethods):
# endregion # 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( async def _file_to_media(
self, file, force_document=False, self, file, force_document=False,
progress_callback=None, attributes=None, thumb=None, progress_callback=None, attributes=None, thumb=None,

View File

@ -12,6 +12,7 @@ import types
from collections import UserList from collections import UserList
from mimetypes import guess_extension from mimetypes import guess_extension
from .extensions import markdown, html
from .tl import TLObject from .tl import TLObject
from .tl.types import ( from .tl.types import (
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
@ -423,6 +424,39 @@ def get_message_id(message):
raise TypeError('Invalid message type: {}'.format(type(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): def get_input_location(location):
"""Similar to :meth:`get_input_peer`, but for input messages.""" """Similar to :meth:`get_input_peer`, but for input messages."""
try: try: