mirror of
				https://github.com/LonamiWebs/Telethon.git
				synced 2025-11-04 01:47:27 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			451 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			451 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import hashlib
 | 
						|
 | 
						|
from .. import functions, types
 | 
						|
from ... import utils
 | 
						|
 | 
						|
_TYPE_TO_MIMES = {
 | 
						|
    'gif': ['image/gif'],  # 'video/mp4' too, but that's used for video
 | 
						|
    'article': ['text/html'],
 | 
						|
    'audio': ['audio/mpeg'],
 | 
						|
    'contact': [],
 | 
						|
    'file': ['application/pdf', 'application/zip'],  # actually any
 | 
						|
    'geo': [],
 | 
						|
    'photo': ['image/jpeg'],
 | 
						|
    'sticker': ['image/webp', 'application/x-tgsticker'],
 | 
						|
    'venue': [],
 | 
						|
    'video': ['video/mp4'],  # tdlib includes text/html for some reason
 | 
						|
    'voice': ['audio/ogg'],
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
class InlineBuilder:
 | 
						|
    """
 | 
						|
    Helper class to allow defining `InlineQuery
 | 
						|
    <telethon.events.inlinequery.InlineQuery>` ``results``.
 | 
						|
 | 
						|
    Common arguments to all methods are
 | 
						|
    explained here to avoid repetition:
 | 
						|
 | 
						|
        text (`str`, optional):
 | 
						|
            If present, the user will send a text
 | 
						|
            message with this text upon being clicked.
 | 
						|
 | 
						|
        link_preview (`bool`, optional):
 | 
						|
            Whether to show a link preview in the sent
 | 
						|
            text message or not.
 | 
						|
 | 
						|
        geo (:tl:`InputGeoPoint`, :tl:`GeoPoint`, :tl:`InputMediaVenue`, :tl:`MessageMediaVenue`, optional):
 | 
						|
            If present, it may either be a geo point or a venue.
 | 
						|
 | 
						|
        period (int, optional):
 | 
						|
            The period in seconds to be used for geo points.
 | 
						|
 | 
						|
        contact (:tl:`InputMediaContact`, :tl:`MessageMediaContact`, optional):
 | 
						|
            If present, it must be the contact information to send.
 | 
						|
 | 
						|
        game (`bool`, optional):
 | 
						|
            May be `True` to indicate that the game will be sent.
 | 
						|
 | 
						|
        buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`, optional):
 | 
						|
            Same as ``buttons`` for `client.send_message()
 | 
						|
            <telethon.client.messages.MessageMethods.send_message>`.
 | 
						|
 | 
						|
        parse_mode (`str`, optional):
 | 
						|
            Same as ``parse_mode`` for `client.send_message()
 | 
						|
            <telethon.client.messageparse.MessageParseMethods.parse_mode>`.
 | 
						|
 | 
						|
        id (`str`, optional):
 | 
						|
            The string ID to use for this result. If not present, it
 | 
						|
            will be the SHA256 hexadecimal digest of converting the
 | 
						|
            created :tl:`InputBotInlineResult` with empty ID to ``bytes()``,
 | 
						|
            so that the ID will be deterministic for the same input.
 | 
						|
 | 
						|
            .. note::
 | 
						|
 | 
						|
                If two inputs are exactly the same, their IDs will be the same
 | 
						|
                too. If you send two articles with the same ID, it will raise
 | 
						|
                ``ResultIdDuplicateError``. Consider giving them an explicit
 | 
						|
                ID if you need to send two results that are the same.
 | 
						|
    """
 | 
						|
    def __init__(self, client):
 | 
						|
        self._client = client
 | 
						|
 | 
						|
    # noinspection PyIncorrectDocstring
 | 
						|
    async def article(
 | 
						|
            self, title, description=None,
 | 
						|
            *, url=None, thumb=None, content=None,
 | 
						|
            id=None, text=None, parse_mode=(), link_preview=True,
 | 
						|
            geo=None, period=60, contact=None, game=False, buttons=None
 | 
						|
    ):
 | 
						|
        """
 | 
						|
        Creates new inline result of article type.
 | 
						|
 | 
						|
        Args:
 | 
						|
            title (`str`):
 | 
						|
                The title to be shown for this result.
 | 
						|
 | 
						|
            description (`str`, optional):
 | 
						|
                Further explanation of what this result means.
 | 
						|
 | 
						|
            url (`str`, optional):
 | 
						|
                The URL to be shown for this result.
 | 
						|
 | 
						|
            thumb (:tl:`InputWebDocument`, optional):
 | 
						|
                The thumbnail to be shown for this result.
 | 
						|
                For now it has to be a :tl:`InputWebDocument` if present.
 | 
						|
 | 
						|
            content (:tl:`InputWebDocument`, optional):
 | 
						|
                The content to be shown for this result.
 | 
						|
                For now it has to be a :tl:`InputWebDocument` if present.
 | 
						|
 | 
						|
        Example:
 | 
						|
            .. code-block:: python
 | 
						|
 | 
						|
                results = [
 | 
						|
                    # Option with title and description sending a message.
 | 
						|
                    builder.article(
 | 
						|
                        title='First option',
 | 
						|
                        description='This is the first option',
 | 
						|
                        text='Text sent after clicking this option',
 | 
						|
                    ),
 | 
						|
                    # Option with title URL to be opened when clicked.
 | 
						|
                    builder.article(
 | 
						|
                        title='Second option',
 | 
						|
                        url='https://example.com',
 | 
						|
                        text='Text sent if the user clicks the option and not the URL',
 | 
						|
                    ),
 | 
						|
                    # Sending a message with buttons.
 | 
						|
                    # You can use a list or a list of lists to include more buttons.
 | 
						|
                    builder.article(
 | 
						|
                        title='Third option',
 | 
						|
                        text='Text sent with buttons below',
 | 
						|
                        buttons=Button.url('https://example.com'),
 | 
						|
                    ),
 | 
						|
                ]
 | 
						|
        """
 | 
						|
        # TODO Does 'article' work always?
 | 
						|
        # article, photo, gif, mpeg4_gif, video, audio,
 | 
						|
        # voice, document, location, venue, contact, game
 | 
						|
        result = types.InputBotInlineResult(
 | 
						|
            id=id or '',
 | 
						|
            type='article',
 | 
						|
            send_message=await self._message(
 | 
						|
                text=text, parse_mode=parse_mode, link_preview=link_preview,
 | 
						|
                geo=geo, period=period,
 | 
						|
                contact=contact,
 | 
						|
                game=game,
 | 
						|
                buttons=buttons
 | 
						|
            ),
 | 
						|
            title=title,
 | 
						|
            description=description,
 | 
						|
            url=url,
 | 
						|
            thumb=thumb,
 | 
						|
            content=content
 | 
						|
        )
 | 
						|
        if id is None:
 | 
						|
            result.id = hashlib.sha256(bytes(result)).hexdigest()
 | 
						|
 | 
						|
        return result
 | 
						|
 | 
						|
    # noinspection PyIncorrectDocstring
 | 
						|
    async def photo(
 | 
						|
            self, file, *, id=None, include_media=True,
 | 
						|
            text=None, parse_mode=(), link_preview=True,
 | 
						|
            geo=None, period=60, contact=None, game=False, buttons=None
 | 
						|
    ):
 | 
						|
        """
 | 
						|
        Creates a new inline result of photo type.
 | 
						|
 | 
						|
        Args:
 | 
						|
            include_media (`bool`, optional):
 | 
						|
                Whether the photo file used to display the result should be
 | 
						|
                included in the message itself or not. By default, the photo
 | 
						|
                is included, and the text parameter alters the caption.
 | 
						|
 | 
						|
            file (`obj`, optional):
 | 
						|
                Same as ``file`` for `client.send_file()
 | 
						|
                <telethon.client.uploads.UploadMethods.send_file>`.
 | 
						|
 | 
						|
        Example:
 | 
						|
            .. code-block:: python
 | 
						|
 | 
						|
                results = [
 | 
						|
                    # Sending just the photo when the user selects it.
 | 
						|
                    builder.photo('/path/to/photo.jpg'),
 | 
						|
 | 
						|
                    # Including a caption with some in-memory photo.
 | 
						|
                    photo_bytesio = ...
 | 
						|
                    builder.photo(
 | 
						|
                        photo_bytesio,
 | 
						|
                        text='This will be the caption of the sent photo',
 | 
						|
                    ),
 | 
						|
 | 
						|
                    # Sending just the message without including the photo.
 | 
						|
                    builder.photo(
 | 
						|
                        photo,
 | 
						|
                        text='This will be a normal text message',
 | 
						|
                        include_media=False,
 | 
						|
                    ),
 | 
						|
                ]
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            fh = utils.get_input_photo(file)
 | 
						|
        except TypeError:
 | 
						|
            _, media, _ = await self._client._file_to_media(
 | 
						|
                file, allow_cache=True, as_image=True
 | 
						|
            )
 | 
						|
            if isinstance(media, types.InputPhoto):
 | 
						|
                fh = media
 | 
						|
            else:
 | 
						|
                r = await self._client(functions.messages.UploadMediaRequest(
 | 
						|
                    types.InputPeerSelf(), media=media
 | 
						|
                ))
 | 
						|
                fh = utils.get_input_photo(r.photo)
 | 
						|
 | 
						|
        result = types.InputBotInlineResultPhoto(
 | 
						|
            id=id or '',
 | 
						|
            type='photo',
 | 
						|
            photo=fh,
 | 
						|
            send_message=await self._message(
 | 
						|
                text=text or '',
 | 
						|
                parse_mode=parse_mode,
 | 
						|
                link_preview=link_preview,
 | 
						|
                media=include_media,
 | 
						|
                geo=geo,
 | 
						|
                period=period,
 | 
						|
                contact=contact,
 | 
						|
                game=game,
 | 
						|
                buttons=buttons
 | 
						|
            )
 | 
						|
        )
 | 
						|
        if id is None:
 | 
						|
            result.id = hashlib.sha256(bytes(result)).hexdigest()
 | 
						|
 | 
						|
        return result
 | 
						|
 | 
						|
    # noinspection PyIncorrectDocstring
 | 
						|
    async def document(
 | 
						|
            self, file, title=None, *, description=None, type=None,
 | 
						|
            mime_type=None, attributes=None, force_document=False,
 | 
						|
            voice_note=False, video_note=False, use_cache=True, id=None,
 | 
						|
            text=None, parse_mode=(), link_preview=True,
 | 
						|
            geo=None, period=60, contact=None, game=False, buttons=None,
 | 
						|
            include_media=True
 | 
						|
    ):
 | 
						|
        """
 | 
						|
        Creates a new inline result of document type.
 | 
						|
 | 
						|
        `use_cache`, `mime_type`, `attributes`, `force_document`,
 | 
						|
        `voice_note` and `video_note` are described in `client.send_file
 | 
						|
        <telethon.client.uploads.UploadMethods.send_file>`.
 | 
						|
 | 
						|
        Args:
 | 
						|
            file (`obj`):
 | 
						|
                Same as ``file`` for `client.send_file()
 | 
						|
                <telethon.client.uploads.UploadMethods.send_file>`.
 | 
						|
 | 
						|
            title (`str`, optional):
 | 
						|
                The title to be shown for this result.
 | 
						|
 | 
						|
            description (`str`, optional):
 | 
						|
                Further explanation of what this result means.
 | 
						|
 | 
						|
            type (`str`, optional):
 | 
						|
                The type of the document. May be one of: article, audio,
 | 
						|
                contact, file, geo, gif, photo, sticker, venue, video, voice.
 | 
						|
                It will be automatically set if ``mime_type`` is specified,
 | 
						|
                and default to ``'file'`` if no matching mime type is found.
 | 
						|
                you may need to pass ``attributes`` in order to use ``type``
 | 
						|
                effectively.
 | 
						|
 | 
						|
            attributes (`list`, optional):
 | 
						|
                Optional attributes that override the inferred ones, like
 | 
						|
                :tl:`DocumentAttributeFilename` and so on.
 | 
						|
 | 
						|
            include_media (`bool`, optional):
 | 
						|
                Whether the document file used to display the result should be
 | 
						|
                included in the message itself or not. By default, the document
 | 
						|
                is included, and the text parameter alters the caption.
 | 
						|
 | 
						|
        Example:
 | 
						|
            .. code-block:: python
 | 
						|
 | 
						|
                results = [
 | 
						|
                    # Sending just the file when the user selects it.
 | 
						|
                    builder.document('/path/to/file.pdf'),
 | 
						|
 | 
						|
                    # Including a caption with some in-memory file.
 | 
						|
                    file_bytesio = ...
 | 
						|
                    builder.document(
 | 
						|
                        file_bytesio,
 | 
						|
                        text='This will be the caption of the sent file',
 | 
						|
                    ),
 | 
						|
 | 
						|
                    # Sending just the message without including the file.
 | 
						|
                    builder.document(
 | 
						|
                        photo,
 | 
						|
                        text='This will be a normal text message',
 | 
						|
                        include_media=False,
 | 
						|
                    ),
 | 
						|
                ]
 | 
						|
        """
 | 
						|
        if type is None:
 | 
						|
            if voice_note:
 | 
						|
                type = 'voice'
 | 
						|
            elif mime_type:
 | 
						|
                for ty, mimes in _TYPE_TO_MIMES.items():
 | 
						|
                    for mime in mimes:
 | 
						|
                        if mime_type == mime:
 | 
						|
                            type = ty
 | 
						|
                            break
 | 
						|
 | 
						|
            if type is None:
 | 
						|
                type = 'file'
 | 
						|
 | 
						|
        try:
 | 
						|
            fh = utils.get_input_document(file)
 | 
						|
        except TypeError:
 | 
						|
            _, media, _ = await self._client._file_to_media(
 | 
						|
                file,
 | 
						|
                mime_type=mime_type,
 | 
						|
                attributes=attributes,
 | 
						|
                force_document=force_document,
 | 
						|
                voice_note=voice_note,
 | 
						|
                video_note=video_note,
 | 
						|
                allow_cache=use_cache
 | 
						|
            )
 | 
						|
            if isinstance(media, types.InputDocument):
 | 
						|
                fh = media
 | 
						|
            else:
 | 
						|
                r = await self._client(functions.messages.UploadMediaRequest(
 | 
						|
                    types.InputPeerSelf(), media=media
 | 
						|
                ))
 | 
						|
                fh = utils.get_input_document(r.document)
 | 
						|
 | 
						|
        result = types.InputBotInlineResultDocument(
 | 
						|
            id=id or '',
 | 
						|
            type=type,
 | 
						|
            document=fh,
 | 
						|
            send_message=await self._message(
 | 
						|
                # Empty string for text if there's media but text is None.
 | 
						|
                # We may want to display a document but send text; however
 | 
						|
                # default to sending the media (without text, i.e. stickers).
 | 
						|
                text=text or '',
 | 
						|
                parse_mode=parse_mode,
 | 
						|
                link_preview=link_preview,
 | 
						|
                media=include_media,
 | 
						|
                geo=geo,
 | 
						|
                period=period,
 | 
						|
                contact=contact,
 | 
						|
                game=game,
 | 
						|
                buttons=buttons
 | 
						|
            ),
 | 
						|
            title=title,
 | 
						|
            description=description
 | 
						|
        )
 | 
						|
        if id is None:
 | 
						|
            result.id = hashlib.sha256(bytes(result)).hexdigest()
 | 
						|
 | 
						|
        return result
 | 
						|
 | 
						|
    # noinspection PyIncorrectDocstring
 | 
						|
    async def game(
 | 
						|
            self, short_name, *, id=None,
 | 
						|
            text=None, parse_mode=(), link_preview=True,
 | 
						|
            geo=None, period=60, contact=None, game=False, buttons=None
 | 
						|
    ):
 | 
						|
        """
 | 
						|
        Creates a new inline result of game type.
 | 
						|
 | 
						|
        Args:
 | 
						|
            short_name (`str`):
 | 
						|
                The short name of the game to use.
 | 
						|
        """
 | 
						|
        result = types.InputBotInlineResultGame(
 | 
						|
            id=id or '',
 | 
						|
            short_name=short_name,
 | 
						|
            send_message=await self._message(
 | 
						|
                text=text, parse_mode=parse_mode, link_preview=link_preview,
 | 
						|
                geo=geo, period=period,
 | 
						|
                contact=contact,
 | 
						|
                game=game,
 | 
						|
                buttons=buttons
 | 
						|
            )
 | 
						|
        )
 | 
						|
        if id is None:
 | 
						|
            result.id = hashlib.sha256(bytes(result)).hexdigest()
 | 
						|
 | 
						|
        return result
 | 
						|
 | 
						|
    async def _message(
 | 
						|
            self, *,
 | 
						|
            text=None, parse_mode=(), link_preview=True, media=False,
 | 
						|
            geo=None, period=60, contact=None, game=False, buttons=None
 | 
						|
    ):
 | 
						|
        # Empty strings are valid but false-y; if they're empty use dummy '\0'
 | 
						|
        args = ('\0' if text == '' else text, geo, contact, game)
 | 
						|
        if sum(1 for x in args if x is not None and x is not False) != 1:
 | 
						|
            raise ValueError(
 | 
						|
                'Must set exactly one of text, geo, contact or game (set {})'
 | 
						|
                .format(', '.join(x[0] for x in zip(
 | 
						|
                    'text geo contact game'.split(), args) if x[1]) or 'none')
 | 
						|
            )
 | 
						|
 | 
						|
        markup = self._client.build_reply_markup(buttons, inline_only=True)
 | 
						|
        if text is not None:
 | 
						|
            text, msg_entities = await self._client._parse_message_text(
 | 
						|
                text, parse_mode
 | 
						|
            )
 | 
						|
            if media:
 | 
						|
                # "MediaAuto" means it will use whatever media the inline
 | 
						|
                # result itself has (stickers, photos, or documents), while
 | 
						|
                # respecting the user's text (caption) and formatting.
 | 
						|
                return types.InputBotInlineMessageMediaAuto(
 | 
						|
                    message=text,
 | 
						|
                    entities=msg_entities,
 | 
						|
                    reply_markup=markup
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                return types.InputBotInlineMessageText(
 | 
						|
                    message=text,
 | 
						|
                    no_webpage=not link_preview,
 | 
						|
                    entities=msg_entities,
 | 
						|
                    reply_markup=markup
 | 
						|
                )
 | 
						|
        elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)):
 | 
						|
            return types.InputBotInlineMessageMediaGeo(
 | 
						|
                geo_point=utils.get_input_geo(geo),
 | 
						|
                period=period,
 | 
						|
                reply_markup=markup
 | 
						|
            )
 | 
						|
        elif isinstance(geo, (types.InputMediaVenue, types.MessageMediaVenue)):
 | 
						|
            if isinstance(geo, types.InputMediaVenue):
 | 
						|
                geo_point = geo.geo_point
 | 
						|
            else:
 | 
						|
                geo_point = geo.geo
 | 
						|
 | 
						|
            return types.InputBotInlineMessageMediaVenue(
 | 
						|
                geo_point=geo_point,
 | 
						|
                title=geo.title,
 | 
						|
                address=geo.address,
 | 
						|
                provider=geo.provider,
 | 
						|
                venue_id=geo.venue_id,
 | 
						|
                venue_type=geo.venue_type,
 | 
						|
                reply_markup=markup
 | 
						|
            )
 | 
						|
        elif isinstance(contact, (
 | 
						|
                types.InputMediaContact, types.MessageMediaContact)):
 | 
						|
            return types.InputBotInlineMessageMediaContact(
 | 
						|
                phone_number=contact.phone_number,
 | 
						|
                first_name=contact.first_name,
 | 
						|
                last_name=contact.last_name,
 | 
						|
                vcard=contact.vcard,
 | 
						|
                reply_markup=markup
 | 
						|
            )
 | 
						|
        elif game:
 | 
						|
            return types.InputBotInlineMessageGame(
 | 
						|
                reply_markup=markup
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            raise ValueError('No text, game or valid geo or contact given')
 |