mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-22 17:36:34 +03:00
Separate file and message methods from TelegramClient
This commit is contained in:
parent
bb9b9796e0
commit
4bd20f1ce2
630
telethon/client/files.py
Normal file
630
telethon/client/files.py
Normal file
|
@ -0,0 +1,630 @@
|
|||
import hashlib
|
||||
import io
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
from mimetypes import guess_type
|
||||
|
||||
from .users import UserMethods
|
||||
from .. import utils, helpers
|
||||
from ..extensions import markdown, html
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
try:
|
||||
import hachoir
|
||||
except ImportError:
|
||||
hachoir = None
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileMethods(UserMethods):
|
||||
|
||||
# region Public methods
|
||||
|
||||
async def send_file(
|
||||
self, entity, file, caption='', force_document=False,
|
||||
progress_callback=None, reply_to=None, attributes=None,
|
||||
thumb=None, allow_cache=True, parse_mode=utils.Default,
|
||||
voice_note=False, video_note=False, **kwargs):
|
||||
"""
|
||||
Sends a file to the specified entity.
|
||||
|
||||
Args:
|
||||
entity (`entity`):
|
||||
Who will receive the file.
|
||||
|
||||
file (`str` | `bytes` | `file` | `media`):
|
||||
The path of the file, byte array, or stream that will be sent.
|
||||
Note that if a byte array or a stream is given, a filename
|
||||
or its type won't be inferred, and it will be sent as an
|
||||
"unnamed application/octet-stream".
|
||||
|
||||
Furthermore the file may be any media (a message, document,
|
||||
photo or similar) so that it can be resent without the need
|
||||
to download and re-upload it again.
|
||||
|
||||
If a list or similar is provided, the files in it will be
|
||||
sent as an album in the order in which they appear, sliced
|
||||
in chunks of 10 if more than 10 are given.
|
||||
|
||||
caption (`str`, optional):
|
||||
Optional caption for the sent media message.
|
||||
|
||||
force_document (`bool`, optional):
|
||||
If left to ``False`` and the file is a path that ends with
|
||||
the extension of an image file or a video file, it will be
|
||||
sent as such. Otherwise always as a document.
|
||||
|
||||
progress_callback (`callable`, optional):
|
||||
A callback function accepting two parameters:
|
||||
``(sent bytes, total)``.
|
||||
|
||||
reply_to (`int` | :tl:`Message`):
|
||||
Same as `reply_to` from `send_message`.
|
||||
|
||||
attributes (`list`, optional):
|
||||
Optional attributes that override the inferred ones, like
|
||||
:tl:`DocumentAttributeFilename` and so on.
|
||||
|
||||
thumb (`str` | `bytes` | `file`, optional):
|
||||
Optional thumbnail (for videos).
|
||||
|
||||
allow_cache (`bool`, optional):
|
||||
Whether to allow using the cached version stored in the
|
||||
database or not. Defaults to ``True`` to avoid re-uploads.
|
||||
Must be ``False`` if you wish to use different attributes
|
||||
or thumb than those that were used when the file was cached.
|
||||
|
||||
parse_mode (`object`, optional):
|
||||
See the `TelegramClient.parse_mode` property for allowed
|
||||
values. Markdown parsing will be used by default.
|
||||
|
||||
voice_note (`bool`, optional):
|
||||
If ``True`` the audio will be sent as a voice note.
|
||||
|
||||
Set `allow_cache` to ``False`` if you sent the same file
|
||||
without this setting before for it to work.
|
||||
|
||||
video_note (`bool`, optional):
|
||||
If ``True`` the video will be sent as a video note,
|
||||
also known as a round video message.
|
||||
|
||||
Set `allow_cache` to ``False`` if you sent the same file
|
||||
without this setting before for it to work.
|
||||
|
||||
Notes:
|
||||
If the ``hachoir3`` package (``hachoir`` module) is installed,
|
||||
it will be used to determine metadata from audio and video files.
|
||||
|
||||
Returns:
|
||||
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.
|
||||
if utils.is_list_like(file):
|
||||
# TODO Fix progress_callback
|
||||
images = []
|
||||
if force_document:
|
||||
documents = file
|
||||
else:
|
||||
documents = []
|
||||
for x in file:
|
||||
if utils.is_image(x):
|
||||
images.append(x)
|
||||
else:
|
||||
documents.append(x)
|
||||
|
||||
result = []
|
||||
while images:
|
||||
result += await self._send_album(
|
||||
entity, images[:10], caption=caption,
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
parse_mode=parse_mode
|
||||
)
|
||||
images = images[10:]
|
||||
|
||||
result.extend(
|
||||
await self.send_file(
|
||||
entity, x, allow_cache=allow_cache,
|
||||
caption=caption, force_document=force_document,
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
attributes=attributes, thumb=thumb, voice_note=voice_note,
|
||||
video_note=video_note, **kwargs
|
||||
) for x in documents
|
||||
)
|
||||
return result
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
reply_to = utils.get_message_id(reply_to)
|
||||
|
||||
# Not document since it's subject to change.
|
||||
# Needed when a Message is passed to send_message and it has media.
|
||||
if 'entities' in kwargs:
|
||||
msg_entities = kwargs['entities']
|
||||
else:
|
||||
caption, msg_entities =\
|
||||
await self._parse_message_text(caption, parse_mode)
|
||||
|
||||
file_handle, media = await self._file_to_media(
|
||||
file, allow_cache=allow_cache)
|
||||
|
||||
request = functions.messages.SendMediaRequest(
|
||||
entity, media, reply_to_msg_id=reply_to, message=caption,
|
||||
entities=msg_entities
|
||||
)
|
||||
msg = self._get_response_message(request, await self(request), entity)
|
||||
self._cache_media(msg, file, file_handle, force_document=force_document)
|
||||
|
||||
return msg
|
||||
|
||||
async def send_voice_note(self, *args, **kwargs):
|
||||
"""Deprecated, see :meth:`send_file`."""
|
||||
warnings.warn('send_voice_note is deprecated, use '
|
||||
'send_file(..., voice_note=True) instead')
|
||||
kwargs['is_voice_note'] = True
|
||||
return await self.send_file(*args, **kwargs)
|
||||
|
||||
async def _send_album(self, entity, files, caption='',
|
||||
progress_callback=None, reply_to=None,
|
||||
parse_mode=utils.Default):
|
||||
"""Specialized version of .send_file for albums"""
|
||||
# We don't care if the user wants to avoid cache, we will use it
|
||||
# anyway. Why? The cached version will be exactly the same thing
|
||||
# we need to produce right now to send albums (uploadMedia), and
|
||||
# cache only makes a difference for documents where the user may
|
||||
# want the attributes used on them to change.
|
||||
#
|
||||
# In theory documents can be sent inside the albums but they appear
|
||||
# as different messages (not inside the album), and the logic to set
|
||||
# the attributes/avoid cache is already written in .send_file().
|
||||
entity = await self.get_input_entity(entity)
|
||||
if not utils.is_list_like(caption):
|
||||
caption = (caption,)
|
||||
captions = [
|
||||
await self._parse_message_text(caption or '', parse_mode)
|
||||
for caption in reversed(caption) # Pop from the end (so reverse)
|
||||
]
|
||||
reply_to = utils.get_message_id(reply_to)
|
||||
|
||||
# Need to upload the media first, but only if they're not cached yet
|
||||
media = []
|
||||
for file in files:
|
||||
# fh will either be InputPhoto or a modified InputFile
|
||||
fh = await self.upload_file(file, use_cache=types.InputPhoto)
|
||||
if not isinstance(fh, types.InputPhoto):
|
||||
r = await self(functions.messages.UploadMediaRequest(
|
||||
entity, media=types.InputMediaUploadedPhoto(fh)
|
||||
))
|
||||
input_photo = utils.get_input_photo(r.photo)
|
||||
self.session.cache_file(fh.md5, fh.size, input_photo)
|
||||
fh = input_photo
|
||||
|
||||
if captions:
|
||||
caption, msg_entities = captions.pop()
|
||||
else:
|
||||
caption, msg_entities = '', None
|
||||
media.append(types.InputSingleMedia(types.InputMediaPhoto(fh), message=caption,
|
||||
entities=msg_entities))
|
||||
|
||||
# Now we can construct the multi-media request
|
||||
result = await self(functions.messages.SendMultiMediaRequest(
|
||||
entity, reply_to_msg_id=reply_to, multi_media=media
|
||||
))
|
||||
return [
|
||||
self._get_response_message(update.id, result, entity)
|
||||
for update in result.updates
|
||||
if isinstance(update, types.UpdateMessageID)
|
||||
]
|
||||
|
||||
async def upload_file(
|
||||
self, file, part_size_kb=None, file_name=None, use_cache=None,
|
||||
progress_callback=None):
|
||||
"""
|
||||
Uploads the specified file and returns a handle (an instance of
|
||||
:tl:`InputFile` or :tl:`InputFileBig`, as required) which can be
|
||||
later used before it expires (they are usable during less than a day).
|
||||
|
||||
Uploading a file will simply return a "handle" to the file stored
|
||||
remotely in the Telegram servers, which can be later used on. This
|
||||
will **not** upload the file to your own chat or any chat at all.
|
||||
|
||||
Args:
|
||||
file (`str` | `bytes` | `file`):
|
||||
The path of the file, byte array, or stream that will be sent.
|
||||
Note that if a byte array or a stream is given, a filename
|
||||
or its type won't be inferred, and it will be sent as an
|
||||
"unnamed application/octet-stream".
|
||||
|
||||
part_size_kb (`int`, optional):
|
||||
Chunk size when uploading files. The larger, the less
|
||||
requests will be made (up to 512KB maximum).
|
||||
|
||||
file_name (`str`, optional):
|
||||
The file name which will be used on the resulting InputFile.
|
||||
If not specified, the name will be taken from the ``file``
|
||||
and if this is not a ``str``, it will be ``"unnamed"``.
|
||||
|
||||
use_cache (`type`, optional):
|
||||
The type of cache to use (currently either :tl:`InputDocument`
|
||||
or :tl:`InputPhoto`). If present and the file is small enough
|
||||
to need the MD5, it will be checked against the database,
|
||||
and if a match is found, the upload won't be made. Instead,
|
||||
an instance of type ``use_cache`` will be returned.
|
||||
|
||||
progress_callback (`callable`, optional):
|
||||
A callback function accepting two parameters:
|
||||
``(sent bytes, total)``.
|
||||
|
||||
Returns:
|
||||
:tl:`InputFileBig` if the file size is larger than 10MB,
|
||||
`telethon.tl.custom.input_sized_file.InputSizedFile`
|
||||
(subclass of :tl:`InputFile`) otherwise.
|
||||
"""
|
||||
if isinstance(file, (types.InputFile, types.InputFileBig)):
|
||||
return file # Already uploaded
|
||||
|
||||
if isinstance(file, str):
|
||||
file_size = os.path.getsize(file)
|
||||
elif isinstance(file, bytes):
|
||||
file_size = len(file)
|
||||
else:
|
||||
file = file.read()
|
||||
file_size = len(file)
|
||||
|
||||
# File will now either be a string or bytes
|
||||
if not part_size_kb:
|
||||
part_size_kb = utils.get_appropriated_part_size(file_size)
|
||||
|
||||
if part_size_kb > 512:
|
||||
raise ValueError('The part size must be less or equal to 512KB')
|
||||
|
||||
part_size = int(part_size_kb * 1024)
|
||||
if part_size % 1024 != 0:
|
||||
raise ValueError(
|
||||
'The part size must be evenly divisible by 1024')
|
||||
|
||||
# Set a default file name if None was specified
|
||||
file_id = helpers.generate_random_long()
|
||||
if not file_name:
|
||||
if isinstance(file, str):
|
||||
file_name = os.path.basename(file)
|
||||
else:
|
||||
file_name = str(file_id)
|
||||
|
||||
# Determine whether the file is too big (over 10MB) or not
|
||||
# Telegram does make a distinction between smaller or larger files
|
||||
is_large = file_size > 10 * 1024 * 1024
|
||||
hash_md5 = hashlib.md5()
|
||||
if not is_large:
|
||||
# Calculate the MD5 hash before anything else.
|
||||
# As this needs to be done always for small files,
|
||||
# might as well do it before anything else and
|
||||
# check the cache.
|
||||
if isinstance(file, str):
|
||||
with open(file, 'rb') as stream:
|
||||
file = stream.read()
|
||||
hash_md5.update(file)
|
||||
if use_cache:
|
||||
cached = self.session.get_file(
|
||||
hash_md5.digest(), file_size, cls=use_cache
|
||||
)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
part_count = (file_size + part_size - 1) // part_size
|
||||
__log__.info('Uploading file of %d bytes in %d chunks of %d',
|
||||
file_size, part_count, part_size)
|
||||
|
||||
with open(file, 'rb') if isinstance(file, str) else BytesIO(file)\
|
||||
as stream:
|
||||
for part_index in range(part_count):
|
||||
# Read the file by in chunks of size part_size
|
||||
part = stream.read(part_size)
|
||||
|
||||
# The SavePartRequest is different depending on whether
|
||||
# the file is too large or not (over or less than 10MB)
|
||||
if is_large:
|
||||
request = functions.upload.SaveBigFilePartRequest(
|
||||
file_id, part_index, part_count, part)
|
||||
else:
|
||||
request = functions.upload.SaveFilePartRequest(
|
||||
file_id, part_index, part)
|
||||
|
||||
result = await self(request)
|
||||
if result:
|
||||
__log__.debug('Uploaded %d/%d', part_index + 1,
|
||||
part_count)
|
||||
if progress_callback:
|
||||
progress_callback(stream.tell(), file_size)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'Failed to upload file part {}.'.format(part_index))
|
||||
|
||||
if is_large:
|
||||
return types.InputFileBig(file_id, part_count, file_name)
|
||||
else:
|
||||
return custom.InputSizedFile(
|
||||
file_id, part_count, file_name, md5=hash_md5, size=file_size
|
||||
)
|
||||
|
||||
# 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,
|
||||
allow_cache=True, voice_note=False, video_note=False):
|
||||
if not file:
|
||||
return None, None
|
||||
|
||||
if not isinstance(file, (str, bytes, io.IOBase)):
|
||||
# The user may pass a Message containing media (or the media,
|
||||
# or anything similar) that should be treated as a file. Try
|
||||
# getting the input media for whatever they passed and send it.
|
||||
try:
|
||||
return None, utils.get_input_media(file)
|
||||
except TypeError:
|
||||
return None, None # Can't turn whatever was given into media
|
||||
|
||||
as_image = utils.is_image(file) and not force_document
|
||||
use_cache = types.InputPhoto if as_image else types.InputDocument
|
||||
file_handle = await self.upload_file(
|
||||
file, progress_callback=progress_callback,
|
||||
use_cache=use_cache if allow_cache else None
|
||||
)
|
||||
|
||||
if isinstance(file_handle, use_cache):
|
||||
# File was cached, so an instance of use_cache was returned
|
||||
if as_image:
|
||||
media = types.InputMediaPhoto(file_handle)
|
||||
else:
|
||||
media = types.InputMediaDocument(file_handle)
|
||||
elif as_image:
|
||||
media = types.InputMediaUploadedPhoto(file_handle)
|
||||
else:
|
||||
mime_type = None
|
||||
if isinstance(file, str):
|
||||
# Determine mime-type and attributes
|
||||
# Take the first element by using [0] since it returns a tuple
|
||||
mime_type = guess_type(file)[0]
|
||||
attr_dict = {
|
||||
types.DocumentAttributeFilename:
|
||||
types.DocumentAttributeFilename(
|
||||
os.path.basename(file))
|
||||
}
|
||||
if utils.is_audio(file) and hachoir:
|
||||
m = hachoir.metadata.extractMetadata(
|
||||
hachoir.parser.createParser(file)
|
||||
)
|
||||
attr_dict[types.DocumentAttributeAudio] = \
|
||||
types.DocumentAttributeAudio(
|
||||
voice=voice_note,
|
||||
title=m.get('title') if m.has(
|
||||
'title') else None,
|
||||
performer=m.get('author') if m.has(
|
||||
'author') else None,
|
||||
duration=int(m.get('duration').seconds
|
||||
if m.has('duration') else 0)
|
||||
)
|
||||
|
||||
if not force_document and utils.is_video(file):
|
||||
if hachoir:
|
||||
m = hachoir.metadata.extractMetadata(
|
||||
hachoir.parser.createParser(file)
|
||||
)
|
||||
doc = types.DocumentAttributeVideo(
|
||||
round_message=video_note,
|
||||
w=m.get('width') if m.has('width') else 0,
|
||||
h=m.get('height') if m.has('height') else 0,
|
||||
duration=int(m.get('duration').seconds
|
||||
if m.has('duration') else 0)
|
||||
)
|
||||
else:
|
||||
doc = types.DocumentAttributeVideo(
|
||||
0, 1, 1, round_message=video_note)
|
||||
|
||||
attr_dict[types.DocumentAttributeVideo] = doc
|
||||
else:
|
||||
attr_dict = {
|
||||
types.DocumentAttributeFilename:
|
||||
types.DocumentAttributeFilename(
|
||||
os.path.basename(
|
||||
getattr(file, 'name',
|
||||
None) or 'unnamed'))
|
||||
}
|
||||
|
||||
if voice_note:
|
||||
if types.DocumentAttributeAudio in attr_dict:
|
||||
attr_dict[types.DocumentAttributeAudio].voice = True
|
||||
else:
|
||||
attr_dict[types.DocumentAttributeAudio] = \
|
||||
types.DocumentAttributeAudio(0, voice=True)
|
||||
|
||||
# Now override the attributes if any. As we have a dict of
|
||||
# {cls: instance}, we can override any class with the list
|
||||
# of attributes provided by the user easily.
|
||||
if attributes:
|
||||
for a in attributes:
|
||||
attr_dict[type(a)] = a
|
||||
|
||||
# Ensure we have a mime type, any; but it cannot be None
|
||||
# 'The "octet-stream" subtype is used to indicate that a body
|
||||
# contains arbitrary binary data.'
|
||||
if not mime_type:
|
||||
mime_type = 'application/octet-stream'
|
||||
|
||||
input_kw = {}
|
||||
if thumb:
|
||||
input_kw['thumb'] = await self.upload_file(thumb)
|
||||
|
||||
media = types.InputMediaUploadedDocument(
|
||||
file=file_handle,
|
||||
mime_type=mime_type,
|
||||
attributes=list(attr_dict.values()),
|
||||
**input_kw
|
||||
)
|
||||
return file_handle, media
|
||||
|
||||
def _cache_media(self, msg, file, file_handle,
|
||||
force_document=False):
|
||||
if file and msg and isinstance(file_handle,
|
||||
custom.InputSizedFile):
|
||||
# There was a response message and we didn't use cached
|
||||
# version, so cache whatever we just sent to the database.
|
||||
md5, size = file_handle.md5, file_handle.size
|
||||
if utils.is_image(file) and not force_document:
|
||||
to_cache = utils.get_input_photo(msg.media.photo)
|
||||
else:
|
||||
to_cache = utils.get_input_document(msg.media.document)
|
||||
self.session.cache_file(md5, size, to_cache)
|
||||
|
||||
# endregion
|
650
telethon/client/messages.py
Normal file
650
telethon/client/messages.py
Normal file
|
@ -0,0 +1,650 @@
|
|||
import asyncio
|
||||
import itertools
|
||||
import logging
|
||||
import time
|
||||
import warnings
|
||||
from collections import UserList
|
||||
|
||||
from .files import FileMethods
|
||||
from .. import utils
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MessageMethods(FileMethods):
|
||||
|
||||
# region Public methods
|
||||
|
||||
# region Message retrieval
|
||||
|
||||
async def iter_messages(
|
||||
self, entity, limit=None, offset_date=None, offset_id=0,
|
||||
max_id=0, min_id=0, add_offset=0, search=None, filter=None,
|
||||
from_user=None, batch_size=100, wait_time=None, ids=None,
|
||||
_total=None):
|
||||
"""
|
||||
Iterator over the message history for the specified entity.
|
||||
|
||||
If either `search`, `filter` or `from_user` are provided,
|
||||
:tl:`messages.Search` will be used instead of :tl:`messages.getHistory`.
|
||||
|
||||
Args:
|
||||
entity (`entity`):
|
||||
The entity from whom to retrieve the message history.
|
||||
|
||||
limit (`int` | `None`, optional):
|
||||
Number of messages to be retrieved. Due to limitations with
|
||||
the API retrieving more than 3000 messages will take longer
|
||||
than half a minute (or even more based on previous calls).
|
||||
The limit may also be ``None``, which would eventually return
|
||||
the whole history.
|
||||
|
||||
offset_date (`datetime`):
|
||||
Offset date (messages *previous* to this date will be
|
||||
retrieved). Exclusive.
|
||||
|
||||
offset_id (`int`):
|
||||
Offset message ID (only messages *previous* to the given
|
||||
ID will be retrieved). Exclusive.
|
||||
|
||||
max_id (`int`):
|
||||
All the messages with a higher (newer) ID or equal to this will
|
||||
be excluded.
|
||||
|
||||
min_id (`int`):
|
||||
All the messages with a lower (older) ID or equal to this will
|
||||
be excluded.
|
||||
|
||||
add_offset (`int`):
|
||||
Additional message offset (all of the specified offsets +
|
||||
this offset = older messages).
|
||||
|
||||
search (`str`):
|
||||
The string to be used as a search query.
|
||||
|
||||
filter (:tl:`MessagesFilter` | `type`):
|
||||
The filter to use when returning messages. For instance,
|
||||
:tl:`InputMessagesFilterPhotos` would yield only messages
|
||||
containing photos.
|
||||
|
||||
from_user (`entity`):
|
||||
Only messages from this user will be returned.
|
||||
|
||||
batch_size (`int`):
|
||||
Messages will be returned in chunks of this size (100 is
|
||||
the maximum). While it makes no sense to modify this value,
|
||||
you are still free to do so.
|
||||
|
||||
wait_time (`int`):
|
||||
Wait time between different :tl:`GetHistoryRequest`. Use this
|
||||
parameter to avoid hitting the ``FloodWaitError`` as needed.
|
||||
If left to ``None``, it will default to 1 second only if
|
||||
the limit is higher than 3000.
|
||||
|
||||
ids (`int`, `list`):
|
||||
A single integer ID (or several IDs) for the message that
|
||||
should be returned. This parameter takes precedence over
|
||||
the rest (which will be ignored if this is set). This can
|
||||
for instance be used to get the message with ID 123 from
|
||||
a channel. Note that if the message doesn't exist, ``None``
|
||||
will appear in its place, so that zipping the list of IDs
|
||||
with the messages can match one-to-one.
|
||||
|
||||
_total (`list`, optional):
|
||||
A single-item list to pass the total parameter by reference.
|
||||
|
||||
Yields:
|
||||
Instances of `telethon.tl.custom.message.Message`.
|
||||
|
||||
Notes:
|
||||
Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to
|
||||
be around 30 seconds per 3000 messages, therefore a sleep of 1
|
||||
second is the default for this limit (or above). You may need
|
||||
an higher limit, so you're free to set the ``batch_size`` that
|
||||
you think may be good.
|
||||
"""
|
||||
# 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 = await self.get_input_entity(entity)
|
||||
|
||||
if ids:
|
||||
if not utils.is_list_like(ids):
|
||||
ids = (ids,)
|
||||
async for x in self._iter_ids(entity, ids, total=_total):
|
||||
yield x
|
||||
return
|
||||
|
||||
# Telegram doesn't like min_id/max_id. If these IDs are low enough
|
||||
# (starting from last_id - 100), the request will return nothing.
|
||||
#
|
||||
# We can emulate their behaviour locally by setting offset = max_id
|
||||
# and simply stopping once we hit a message with ID <= min_id.
|
||||
offset_id = max(offset_id, max_id)
|
||||
if offset_id and min_id:
|
||||
if offset_id - min_id <= 1:
|
||||
return
|
||||
|
||||
limit = float('inf') if limit is None else int(limit)
|
||||
if search is not None or filter or from_user:
|
||||
if filter is None:
|
||||
filter = types.InputMessagesFilterEmpty()
|
||||
request = functions.messages.SearchRequest(
|
||||
peer=entity,
|
||||
q=search or '',
|
||||
filter=filter() if isinstance(filter, type) else filter,
|
||||
min_date=None,
|
||||
max_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
add_offset=add_offset,
|
||||
limit=1,
|
||||
max_id=0,
|
||||
min_id=0,
|
||||
hash=0,
|
||||
from_id=(
|
||||
await self.get_input_entity(from_user)
|
||||
if from_user else None
|
||||
)
|
||||
)
|
||||
else:
|
||||
request = functions.messages.GetHistoryRequest(
|
||||
peer=entity,
|
||||
limit=1,
|
||||
offset_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
min_id=0,
|
||||
max_id=0,
|
||||
add_offset=add_offset,
|
||||
hash=0
|
||||
)
|
||||
|
||||
if limit == 0:
|
||||
if not _total:
|
||||
return
|
||||
# No messages, but we still need to know the total message count
|
||||
result = await self(request)
|
||||
if isinstance(result, types.messages.MessagesNotModified):
|
||||
_total[0] = result.count
|
||||
else:
|
||||
_total[0] = getattr(result, 'count', len(result.messages))
|
||||
return
|
||||
|
||||
if wait_time is None:
|
||||
wait_time = 1 if limit > 3000 else 0
|
||||
|
||||
have = 0
|
||||
last_id = float('inf')
|
||||
batch_size = min(max(batch_size, 1), 100)
|
||||
while have < limit:
|
||||
start = asyncio.get_event_loop().time()
|
||||
# Telegram has a hard limit of 100
|
||||
request.limit = min(limit - have, batch_size)
|
||||
r = await self(request)
|
||||
if _total:
|
||||
_total[0] = getattr(r, 'count', len(r.messages))
|
||||
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in itertools.chain(r.users, r.chats)}
|
||||
|
||||
for message in r.messages:
|
||||
if message.id <= min_id:
|
||||
return
|
||||
|
||||
if isinstance(message, types.MessageEmpty)\
|
||||
or message.id >= last_id:
|
||||
continue
|
||||
|
||||
# There has been reports that on bad connections this method
|
||||
# was returning duplicated IDs sometimes. Using ``last_id``
|
||||
# is an attempt to avoid these duplicates, since the message
|
||||
# IDs are returned in descending order.
|
||||
last_id = message.id
|
||||
|
||||
yield custom.Message(self, message, entities, entity)
|
||||
have += 1
|
||||
|
||||
if len(r.messages) < request.limit:
|
||||
break
|
||||
|
||||
request.offset_id = r.messages[-1].id
|
||||
if isinstance(request, functions.messages.GetHistoryRequest):
|
||||
request.offset_date = r.messages[-1].date
|
||||
else:
|
||||
request.max_date = r.messages[-1].date
|
||||
|
||||
time.sleep(max(wait_time - (time.time() - start), 0))
|
||||
|
||||
async def get_messages(self, *args, **kwargs):
|
||||
"""
|
||||
Same as :meth:`iter_messages`, but returns a list instead
|
||||
with an additional ``.total`` attribute on the list.
|
||||
|
||||
If the `limit` is not set, it will be 1 by default unless both
|
||||
`min_id` **and** `max_id` are set (as *named* arguments), in
|
||||
which case the entire range will be returned.
|
||||
|
||||
This is so because any integer limit would be rather arbitrary and
|
||||
it's common to only want to fetch one message, but if a range is
|
||||
specified it makes sense that it should return the entirety of it.
|
||||
|
||||
If `ids` is present in the *named* arguments and is not a list,
|
||||
a single :tl:`Message` will be returned for convenience instead
|
||||
of a list.
|
||||
"""
|
||||
total = [0]
|
||||
kwargs['_total'] = total
|
||||
if len(args) == 1 and 'limit' not in kwargs:
|
||||
if 'min_id' in kwargs and 'max_id' in kwargs:
|
||||
kwargs['limit'] = None
|
||||
else:
|
||||
kwargs['limit'] = 1
|
||||
|
||||
msgs = UserList(x async for x in self.iter_messages(*args, **kwargs))
|
||||
msgs.total = total[0]
|
||||
if 'ids' in kwargs and not utils.is_list_like(kwargs['ids']):
|
||||
return msgs[0]
|
||||
|
||||
return msgs
|
||||
|
||||
async def get_message_history(self, *args, **kwargs):
|
||||
"""Deprecated, see :meth:`get_messages`."""
|
||||
warnings.warn(
|
||||
'get_message_history is deprecated, use get_messages instead'
|
||||
)
|
||||
return await self.get_messages(*args, **kwargs)
|
||||
|
||||
# endregion
|
||||
|
||||
# region Message sending/editing/deleting
|
||||
|
||||
async def send_message(
|
||||
self, entity, message='', reply_to=None,
|
||||
parse_mode=utils.Default, link_preview=True, file=None,
|
||||
force_document=False, clear_draft=False):
|
||||
"""
|
||||
Sends the given message to the specified entity (user/chat/channel).
|
||||
|
||||
The default parse mode is the same as the official applications
|
||||
(a custom flavour of markdown). ``**bold**, `code` or __italic__``
|
||||
are available. In addition you can send ``[links](https://example.com)``
|
||||
and ``[mentions](@username)`` (or using IDs like in the Bot API:
|
||||
``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three
|
||||
backticks.
|
||||
|
||||
Sending a ``/start`` command with a parameter (like ``?start=data``)
|
||||
is also done through this method. Simply send ``'/start data'`` to
|
||||
the bot.
|
||||
|
||||
Args:
|
||||
entity (`entity`):
|
||||
To who will it be sent.
|
||||
|
||||
message (`str` | :tl:`Message`):
|
||||
The message to be sent, or another message object to resend.
|
||||
|
||||
The maximum length for a message is 35,000 bytes or 4,096
|
||||
characters. Longer messages will not be sliced automatically,
|
||||
and you should slice them manually if the text to send is
|
||||
longer than said length.
|
||||
|
||||
reply_to (`int` | :tl:`Message`, optional):
|
||||
Whether to reply to a message or not. If an integer is provided,
|
||||
it should be the ID of the message that it should reply to.
|
||||
|
||||
parse_mode (`object`, optional):
|
||||
See the `TelegramClient.parse_mode` property for allowed
|
||||
values. Markdown parsing will be used by default.
|
||||
|
||||
link_preview (`bool`, optional):
|
||||
Should the link preview be shown?
|
||||
|
||||
file (`file`, optional):
|
||||
Sends a message with a file attached (e.g. a photo,
|
||||
video, audio or document). The ``message`` may be empty.
|
||||
|
||||
force_document (`bool`, optional):
|
||||
Whether to send the given file as a document or not.
|
||||
|
||||
clear_draft (`bool`, optional):
|
||||
Whether the existing draft should be cleared or not.
|
||||
Has no effect when sending a file.
|
||||
|
||||
Returns:
|
||||
The sent `telethon.tl.custom.message.Message`.
|
||||
"""
|
||||
if file is not None:
|
||||
return await self.send_file(
|
||||
entity, file, caption=message, reply_to=reply_to,
|
||||
parse_mode=parse_mode, force_document=force_document
|
||||
)
|
||||
elif not message:
|
||||
raise ValueError(
|
||||
'The message cannot be empty unless a file is provided'
|
||||
)
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
if isinstance(message, types.Message):
|
||||
if (message.media and not isinstance(
|
||||
message.media, types.MessageMediaWebPage)):
|
||||
return await self.send_file(
|
||||
entity, message.media, caption=message.message,
|
||||
entities=message.entities
|
||||
)
|
||||
|
||||
if reply_to is not None:
|
||||
reply_id = utils.get_message_id(reply_to)
|
||||
elif utils.get_peer_id(entity) == utils.get_peer_id(message.to_id):
|
||||
reply_id = message.reply_to_msg_id
|
||||
else:
|
||||
reply_id = None
|
||||
request = functions.messages.SendMessageRequest(
|
||||
peer=entity,
|
||||
message=message.message or '',
|
||||
silent=message.silent,
|
||||
reply_to_msg_id=reply_id,
|
||||
reply_markup=message.reply_markup,
|
||||
entities=message.entities,
|
||||
clear_draft=clear_draft,
|
||||
no_webpage=not isinstance(
|
||||
message.media, types.MessageMediaWebPage)
|
||||
)
|
||||
message = message.message
|
||||
else:
|
||||
message, msg_ent = await self._parse_message_text(message,
|
||||
parse_mode)
|
||||
request = functions.messages.SendMessageRequest(
|
||||
peer=entity,
|
||||
message=message,
|
||||
entities=msg_ent,
|
||||
no_webpage=not link_preview,
|
||||
reply_to_msg_id=utils.get_message_id(reply_to),
|
||||
clear_draft=clear_draft
|
||||
)
|
||||
|
||||
result = await self(request)
|
||||
if isinstance(result, types.UpdateShortSentMessage):
|
||||
to_id, cls = utils.resolve_id(utils.get_peer_id(entity))
|
||||
return custom.Message(self, types.Message(
|
||||
id=result.id,
|
||||
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, entity)
|
||||
|
||||
async def forward_messages(self, entity, messages, from_peer=None):
|
||||
"""
|
||||
Forwards the given message(s) to the specified entity.
|
||||
|
||||
Args:
|
||||
entity (`entity`):
|
||||
To which entity the message(s) will be forwarded.
|
||||
|
||||
messages (`list` | `int` | :tl:`Message`):
|
||||
The message(s) to forward, or their integer IDs.
|
||||
|
||||
from_peer (`entity`):
|
||||
If the given messages are integer IDs and not instances
|
||||
of the ``Message`` class, this *must* be specified in
|
||||
order for the forward to work.
|
||||
|
||||
Returns:
|
||||
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:
|
||||
messages = (messages,)
|
||||
|
||||
if not from_peer:
|
||||
try:
|
||||
# On private chats (to_id = PeerUser), if the message is
|
||||
# not outgoing, we actually need to use "from_id" to get
|
||||
# the conversation on which the message was sent.
|
||||
from_peer = next(
|
||||
m.from_id
|
||||
if not m.out and isinstance(m.to_id, types.PeerUser)
|
||||
else m.to_id for m in messages
|
||||
if isinstance(m, types.Message)
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(
|
||||
'from_chat must be given if integer IDs are used'
|
||||
)
|
||||
|
||||
req = functions.messages.ForwardMessagesRequest(
|
||||
from_peer=from_peer,
|
||||
id=[m if isinstance(m, int) else m.id for m in messages],
|
||||
to_peer=entity
|
||||
)
|
||||
result = await self(req)
|
||||
if isinstance(result, (types.Updates, types.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, types.UpdateMessageID):
|
||||
random_to_id[update.random_id] = update.id
|
||||
elif isinstance(update, (
|
||||
types.UpdateNewMessage, types.UpdateNewChannelMessage)):
|
||||
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
|
||||
|
||||
async def edit_message(
|
||||
self, entity, message=None, text=None, parse_mode=utils.Default,
|
||||
link_preview=True, file=None):
|
||||
"""
|
||||
Edits the given message ID (to change its contents or disable preview).
|
||||
|
||||
Args:
|
||||
entity (`entity` | :tl:`Message`):
|
||||
From which chat to edit the message. This can also be
|
||||
the message to be edited, and the entity will be inferred
|
||||
from it, so the next parameter will be assumed to be the
|
||||
message text.
|
||||
|
||||
message (`int` | :tl:`Message` | `str`):
|
||||
The ID of the message (or :tl:`Message` itself) to be edited.
|
||||
If the `entity` was a :tl:`Message`, then this message will be
|
||||
treated as the new text.
|
||||
|
||||
text (`str`, optional):
|
||||
The new text of the message. Does nothing if the `entity`
|
||||
was a :tl:`Message`.
|
||||
|
||||
parse_mode (`object`, optional):
|
||||
See the `TelegramClient.parse_mode` property for allowed
|
||||
values. Markdown parsing will be used by default.
|
||||
|
||||
link_preview (`bool`, optional):
|
||||
Should the link preview be shown?
|
||||
|
||||
file (`str` | `bytes` | `file` | `media`, optional):
|
||||
The file object that should replace the existing media
|
||||
in the message.
|
||||
|
||||
Examples:
|
||||
|
||||
>>> client = ...
|
||||
>>> message = client.send_message('username', 'hello')
|
||||
>>>
|
||||
>>> client.edit_message('username', message, 'hello!')
|
||||
>>> # or
|
||||
>>> client.edit_message('username', message.id, 'Hello')
|
||||
>>> # or
|
||||
>>> client.edit_message(message, 'Hello!')
|
||||
|
||||
Raises:
|
||||
``MessageAuthorRequiredError`` if you're not the author of the
|
||||
message but tried editing it anyway.
|
||||
|
||||
``MessageNotModifiedError`` if the contents of the message were
|
||||
not modified at all.
|
||||
|
||||
Returns:
|
||||
The edited `telethon.tl.custom.message.Message`.
|
||||
"""
|
||||
if isinstance(entity, types.Message):
|
||||
text = message # Shift the parameters to the right
|
||||
message = entity
|
||||
entity = entity.to_id
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
text, msg_entities = await self._parse_message_text(text, parse_mode)
|
||||
file_handle, media = await self._file_to_media(file)
|
||||
request = functions.messages.EditMessageRequest(
|
||||
peer=entity,
|
||||
id=utils.get_message_id(message),
|
||||
message=text,
|
||||
no_webpage=not link_preview,
|
||||
entities=msg_entities,
|
||||
media=media
|
||||
)
|
||||
msg = self._get_response_message(request, self(request), entity)
|
||||
self._cache_media(msg, file, file_handle)
|
||||
return msg
|
||||
|
||||
async def delete_messages(self, entity, message_ids, revoke=True):
|
||||
"""
|
||||
Deletes a message from a chat, optionally "for everyone".
|
||||
|
||||
Args:
|
||||
entity (`entity`):
|
||||
From who the message will be deleted. This can actually
|
||||
be ``None`` for normal chats, but **must** be present
|
||||
for channels and megagroups.
|
||||
|
||||
message_ids (`list` | `int` | :tl:`Message`):
|
||||
The IDs (or ID) or messages to be deleted.
|
||||
|
||||
revoke (`bool`, optional):
|
||||
Whether the message should be deleted for everyone or not.
|
||||
By default it has the opposite behaviour of official clients,
|
||||
and it will delete the message for everyone.
|
||||
This has no effect on channels or megagroups.
|
||||
|
||||
Returns:
|
||||
A list of :tl:`AffectedMessages`, each item being the result
|
||||
for the delete calls of the messages in chunks of 100 each.
|
||||
"""
|
||||
if not utils.is_list_like(message_ids):
|
||||
message_ids = (message_ids,)
|
||||
|
||||
message_ids = (
|
||||
m.id if isinstance(m, (
|
||||
types.Message, types.MessageService, types.MessageEmpty))
|
||||
else int(m) for m in message_ids
|
||||
)
|
||||
|
||||
entity = await self.get_input_entity(entity) if entity else None
|
||||
if isinstance(entity, types.InputPeerChannel):
|
||||
return await self([functions.channels.DeleteMessagesRequest(
|
||||
entity, list(c)) for c in utils.chunks(message_ids)])
|
||||
else:
|
||||
return await self([functions.messages.DeleteMessagesRequest(
|
||||
list(c), revoke) for c in utils.chunks(message_ids)])
|
||||
|
||||
# endregion
|
||||
|
||||
# region Miscellaneous
|
||||
|
||||
async def send_read_acknowledge(self, entity, message=None, max_id=None,
|
||||
clear_mentions=False):
|
||||
"""
|
||||
Sends a "read acknowledge" (i.e., notifying the given peer that we've
|
||||
read their messages, also known as the "double check").
|
||||
|
||||
This effectively marks a message as read (or more than one) in the
|
||||
given conversation.
|
||||
|
||||
Args:
|
||||
entity (`entity`):
|
||||
The chat where these messages are located.
|
||||
|
||||
message (`list` | :tl:`Message`):
|
||||
Either a list of messages or a single message.
|
||||
|
||||
max_id (`int`):
|
||||
Overrides messages, until which message should the
|
||||
acknowledge should be sent.
|
||||
|
||||
clear_mentions (`bool`):
|
||||
Whether the mention badge should be cleared (so that
|
||||
there are no more mentions) or not for the given entity.
|
||||
|
||||
If no message is provided, this will be the only action
|
||||
taken.
|
||||
"""
|
||||
if max_id is None:
|
||||
if message:
|
||||
if utils.is_list_like(message):
|
||||
max_id = max(msg.id for msg in message)
|
||||
else:
|
||||
max_id = message.id
|
||||
elif not clear_mentions:
|
||||
raise ValueError(
|
||||
'Either a message list or a max_id must be provided.')
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
if clear_mentions:
|
||||
await self(functions.messages.ReadMentionsRequest(entity))
|
||||
if max_id is None:
|
||||
return True
|
||||
|
||||
if max_id is not None:
|
||||
if isinstance(entity, types.InputPeerChannel):
|
||||
return await self(functions.channels.ReadHistoryRequest(
|
||||
entity, max_id=max_id))
|
||||
else:
|
||||
return await self(functions.messages.ReadHistoryRequest(
|
||||
entity, max_id=max_id))
|
||||
|
||||
return False
|
||||
|
||||
# endregion
|
||||
|
||||
# endregion
|
||||
|
||||
# region Private methods
|
||||
|
||||
async def _iter_ids(self, entity, ids, total):
|
||||
"""
|
||||
Special case for `iter_messages` when it should only fetch some IDs.
|
||||
"""
|
||||
if total:
|
||||
total[0] = len(ids)
|
||||
|
||||
if isinstance(entity, types.InputPeerChannel):
|
||||
r = await self(functions.channels.GetMessagesRequest(entity, ids))
|
||||
else:
|
||||
r = await self(functions.messages.GetMessagesRequest(ids))
|
||||
|
||||
if isinstance(r, types.messages.MessagesNotModified):
|
||||
for _ in ids:
|
||||
yield None
|
||||
return
|
||||
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in itertools.chain(r.users, r.chats)}
|
||||
|
||||
# Telegram seems to return the messages in the order in which
|
||||
# we asked them for, so we don't need to check it ourselves.
|
||||
for message in r.messages:
|
||||
if isinstance(message, types.MessageEmpty):
|
||||
yield None
|
||||
else:
|
||||
yield custom.Message(self, message, entities, entity)
|
||||
|
||||
# endregion
|
File diff suppressed because it is too large
Load Diff
|
@ -402,6 +402,27 @@ def get_input_message(message):
|
|||
_raise_cast_fail(message, 'InputMedia')
|
||||
|
||||
|
||||
def get_message_id(message):
|
||||
"""Sanitizes the 'reply_to' parameter a user may send"""
|
||||
if message is None:
|
||||
return None
|
||||
|
||||
if isinstance(message, int):
|
||||
return message
|
||||
|
||||
if hasattr(message, 'original_message'):
|
||||
return message.original_message.id
|
||||
|
||||
try:
|
||||
if message.SUBCLASS_OF_ID == 0x790009e3:
|
||||
# hex(crc32(b'Message')) = 0x790009e3
|
||||
return message.id
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
raise TypeError('Invalid message type: {}'.format(type(message)))
|
||||
|
||||
|
||||
def get_input_location(location):
|
||||
"""Similar to :meth:`get_input_peer`, but for input messages."""
|
||||
try:
|
||||
|
|
Loading…
Reference in New Issue
Block a user