mirror of
				https://github.com/LonamiWebs/Telethon.git
				synced 2025-11-04 09:57:29 +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')
 | 
					    _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):
 | 
					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:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user