mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-22 09:26:37 +03:00
Continue implementing file and message
This commit is contained in:
parent
4df1f4537b
commit
25a2b53d3f
|
@ -356,6 +356,7 @@ Migrating from aiogram
|
|||
Using one of the examples from their v3 documentation with logging and comments removed:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiogram import Bot, Dispatcher, types
|
||||
|
@ -389,6 +390,7 @@ We can see a specific handler for the ``/start`` command and a catch-all echo ha
|
|||
In Telethon:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio, html
|
||||
|
||||
from telethon import Client, RpcError, types, events
|
||||
|
|
|
@ -98,6 +98,8 @@ Each of them can have an additional namespace (as seen above with ``account.``).
|
|||
* ``tl.types`` contains concrete instances, the "bare" types Telegram actually returns.
|
||||
You'll probably use these with :func:`isinstance` a lot.
|
||||
|
||||
Most custom :mod:`types` will also have a private ``_raw`` attribute with the original value from Telegram.
|
||||
|
||||
|
||||
Raw API has a reduced feature-set
|
||||
---------------------------------
|
||||
|
@ -262,10 +264,10 @@ This means any method returning data from Telegram must have a custom wrapper ob
|
|||
Because the standards are higher, the barrier of entry for new additions and features is higher too.
|
||||
|
||||
|
||||
No message.raw_text or message.message
|
||||
--------------------------------------
|
||||
Removed or renamed message properties and methods
|
||||
-------------------------------------------------
|
||||
|
||||
Messages no longer have ``.raw_text`` or ``.message`` properties.
|
||||
Messages no longer have ``raw_text`` or ``message`` properties.
|
||||
|
||||
Instead, you can access the :attr:`types.Message.text`,
|
||||
:attr:`~types.Message.text_markdown` or :attr:`~types.Message.text_html`.
|
||||
|
@ -279,6 +281,25 @@ Those coming from the raw API had no client, so ``text`` couldn't know how to fo
|
|||
Overall, the old design made the parse mode be pretty hidden.
|
||||
This was not very intuitive and also made it very awkward to combine multiple parse modes.
|
||||
|
||||
The ``forward`` property is now :attr:`~types.Message.forward_info`.
|
||||
The ``forward_to`` method is now simply :meth:`~types.Message.forward`.
|
||||
This makes it more consistent with the rest of message methods.
|
||||
|
||||
The ``is_reply``, ``reply_to_msg_id`` and ``reply_to`` properties are now :attr:`~types.Message.replied_message_id`.
|
||||
The ``get_reply_message`` method is now :meth:`~types.Message.get_replied_message`.
|
||||
This should make it clear that you are not getting a reply to the current message, but rather the message it replied to.
|
||||
|
||||
The ``to_id``, ``via_input_bot``, ``action_entities``, ``button_count`` properties are also gone.
|
||||
Some were kept for backwards-compatibility, some were redundant.
|
||||
|
||||
The ``click`` method no longer exists in the message.
|
||||
Instead, find the right :attr:`~types.Message.buttons` to click on.
|
||||
|
||||
The ``download`` method no longer exists in the message.
|
||||
Instead, use :attr:`~types.File.download` on the message's :attr:`~types.Message.file`.
|
||||
|
||||
HMMMM WEB_PREVIEW VS LINK_PREVIEW... probs use link. we're previewing a link not the web
|
||||
|
||||
|
||||
Event and filters are now separate
|
||||
----------------------------------
|
||||
|
|
|
@ -158,18 +158,20 @@ class ProfilePhotoList(AsyncList[File]):
|
|||
)
|
||||
|
||||
if isinstance(result, types.photos.Photos):
|
||||
self._buffer.extend(
|
||||
filter(None, (File._try_from_raw_photo(p) for p in result.photos))
|
||||
)
|
||||
photos = result.photos
|
||||
self._total = len(result.photos)
|
||||
elif isinstance(result, types.photos.PhotosSlice):
|
||||
self._buffer.extend(
|
||||
filter(None, (File._try_from_raw_photo(p) for p in result.photos))
|
||||
)
|
||||
photos = result.photos
|
||||
self._total = result.count
|
||||
else:
|
||||
raise RuntimeError("unexpected case")
|
||||
|
||||
self._buffer.extend(
|
||||
filter(
|
||||
None, (File._try_from_raw_photo(self._client, p) for p in photos)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_profile_photos(self: Client, chat: ChatLike) -> AsyncList[File]:
|
||||
return ProfilePhotoList(self, chat)
|
||||
|
|
|
@ -1235,7 +1235,8 @@ class Client:
|
|||
*,
|
||||
markdown: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
link_preview: Optional[bool] = None,
|
||||
link_preview: bool = False,
|
||||
reply_to: Optional[int] = None,
|
||||
) -> Message:
|
||||
"""
|
||||
Send a message.
|
||||
|
@ -1254,6 +1255,17 @@ class Client:
|
|||
:param text_html:
|
||||
Message text, parsed as HTML.
|
||||
|
||||
:param link_preview:
|
||||
Whether the link preview is allowed.
|
||||
|
||||
Setting this to :data:`True` does not guarantee a preview.
|
||||
Telegram must be able to generate a preview from the first link in the message text.
|
||||
|
||||
To regenerate the preview, send the link to `@WebpageBot <https://t.me/WebpageBot>`_.
|
||||
|
||||
:param reply_to:
|
||||
The message identifier of the message to reply to.
|
||||
|
||||
Note that exactly one *text* parameter must be provided.
|
||||
|
||||
See the section on :doc:`/concepts/messages` to learn about message formatting.
|
||||
|
@ -1265,7 +1277,13 @@ class Client:
|
|||
await client.send_message(chat, markdown='**Hello!**')
|
||||
"""
|
||||
return await send_message(
|
||||
self, chat, text, markdown=markdown, html=html, link_preview=link_preview
|
||||
self,
|
||||
chat,
|
||||
text,
|
||||
markdown=markdown,
|
||||
html=html,
|
||||
link_preview=link_preview,
|
||||
reply_to=reply_to,
|
||||
)
|
||||
|
||||
async def send_photo(
|
||||
|
|
|
@ -15,6 +15,7 @@ from ..types import (
|
|||
OutFileLike,
|
||||
OutWrapper,
|
||||
)
|
||||
from ..types.file import expand_stripped_size
|
||||
from ..utils import generate_random_id
|
||||
from .messages import parse_message
|
||||
|
||||
|
@ -182,8 +183,9 @@ async def send_file(
|
|||
muted=muted,
|
||||
)
|
||||
message, entities = parse_message(
|
||||
text=caption, markdown=caption_markdown, html=caption_html
|
||||
text=caption, markdown=caption_markdown, html=caption_html, allow_empty=True
|
||||
)
|
||||
assert isinstance(message, str)
|
||||
|
||||
peer = (await self._resolve_to_packed(chat))._to_input_peer()
|
||||
|
||||
|
@ -312,6 +314,9 @@ class FileBytesList(AsyncList[bytes]):
|
|||
self._client = client
|
||||
self._loc = file._input_location()
|
||||
self._offset = 0
|
||||
if isinstance(file._thumb, types.PhotoStrippedSize):
|
||||
self._buffer.append(expand_stripped_size(file._thumb.bytes))
|
||||
self._done = True
|
||||
|
||||
async def _fetch_next(self) -> None:
|
||||
result = await self._client(
|
||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, TypeVar, Union
|
||||
|
||||
from ...session import PackedChat
|
||||
from ...tl import abcs, functions, types
|
||||
|
@ -16,11 +16,15 @@ if TYPE_CHECKING:
|
|||
|
||||
def parse_message(
|
||||
*,
|
||||
text: Optional[str] = None,
|
||||
markdown: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
) -> Tuple[str, Optional[List[abcs.MessageEntity]]]:
|
||||
if sum((text is not None, markdown is not None, html is not None)) != 1:
|
||||
text: Optional[Union[str, Message]],
|
||||
markdown: Optional[str],
|
||||
html: Optional[str],
|
||||
allow_empty: bool,
|
||||
) -> Tuple[Union[str, Message], Optional[List[abcs.MessageEntity]]]:
|
||||
cnt = sum((text is not None, markdown is not None, html is not None))
|
||||
if cnt != 1:
|
||||
if cnt == 0 and allow_empty:
|
||||
return "", None
|
||||
raise ValueError("must specify exactly one of text, markdown or html")
|
||||
|
||||
if text is not None:
|
||||
|
@ -38,14 +42,17 @@ def parse_message(
|
|||
async def send_message(
|
||||
self: Client,
|
||||
chat: ChatLike,
|
||||
text: Optional[str] = None,
|
||||
text: Optional[Union[str, Message]] = None,
|
||||
*,
|
||||
markdown: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
link_preview: Optional[bool] = None,
|
||||
reply_to: Optional[int] = None,
|
||||
) -> Message:
|
||||
peer = (await self._resolve_to_packed(chat))._to_input_peer()
|
||||
message, entities = parse_message(text=text, markdown=markdown, html=html)
|
||||
message, entities = parse_message(
|
||||
text=text, markdown=markdown, html=html, allow_empty=False
|
||||
)
|
||||
random_id = generate_random_id()
|
||||
return self._build_message_map(
|
||||
await self(
|
||||
|
@ -57,7 +64,11 @@ async def send_message(
|
|||
noforwards=False,
|
||||
update_stickersets_order=False,
|
||||
peer=peer,
|
||||
reply_to=None,
|
||||
reply_to=types.InputReplyToMessage(
|
||||
reply_to_msg_id=reply_to, top_msg_id=None
|
||||
)
|
||||
if reply_to
|
||||
else None,
|
||||
message=message,
|
||||
random_id=random_id,
|
||||
reply_markup=None,
|
||||
|
@ -65,6 +76,27 @@ async def send_message(
|
|||
schedule_date=None,
|
||||
send_as=None,
|
||||
)
|
||||
if isinstance(message, str)
|
||||
else functions.messages.send_message(
|
||||
no_webpage=not message.web_preview,
|
||||
silent=message.silent,
|
||||
background=False,
|
||||
clear_draft=False,
|
||||
noforwards=not message.can_forward,
|
||||
update_stickersets_order=False,
|
||||
peer=peer,
|
||||
reply_to=types.InputReplyToMessage(
|
||||
reply_to_msg_id=message.replied_message_id, top_msg_id=None
|
||||
)
|
||||
if message.replied_message_id
|
||||
else None,
|
||||
message=message.text or "",
|
||||
random_id=random_id,
|
||||
reply_markup=getattr(message._raw, "reply_markup", None),
|
||||
entities=getattr(message._raw, "entities", None) or None,
|
||||
schedule_date=None,
|
||||
send_as=None,
|
||||
)
|
||||
),
|
||||
peer,
|
||||
).with_random_id(random_id)
|
||||
|
@ -81,7 +113,10 @@ async def edit_message(
|
|||
link_preview: Optional[bool] = None,
|
||||
) -> Message:
|
||||
peer = (await self._resolve_to_packed(chat))._to_input_peer()
|
||||
message, entities = parse_message(text=text, markdown=markdown, html=html)
|
||||
message, entities = parse_message(
|
||||
text=text, markdown=markdown, html=html, allow_empty=False
|
||||
)
|
||||
assert isinstance(message, str)
|
||||
return self._build_message_map(
|
||||
await self(
|
||||
functions.messages.edit_message(
|
||||
|
@ -232,8 +267,8 @@ class HistoryList(MessageList):
|
|||
|
||||
self._extend_buffer(self._client, result)
|
||||
self._limit -= len(self._buffer)
|
||||
self._done = not self._limit
|
||||
if self._buffer:
|
||||
self._done |= not self._limit
|
||||
if self._buffer and not self._done:
|
||||
last = self._last_non_empty_message()
|
||||
self._offset_id = self._buffer[-1].id
|
||||
if (date := getattr(last, "date", None)) is not None:
|
||||
|
@ -268,7 +303,7 @@ class CherryPickedList(MessageList):
|
|||
self._client = client
|
||||
self._chat = chat
|
||||
self._packed: Optional[PackedChat] = None
|
||||
self._ids = ids
|
||||
self._ids: List[abcs.InputMessage] = [types.InputMessageId(id=id) for id in ids]
|
||||
|
||||
async def _fetch_next(self) -> None:
|
||||
if not self._ids:
|
||||
|
@ -279,15 +314,12 @@ class CherryPickedList(MessageList):
|
|||
if self._packed.is_channel():
|
||||
result = await self._client(
|
||||
functions.channels.get_messages(
|
||||
channel=self._packed._to_input_channel(),
|
||||
id=[types.InputMessageId(id=id) for id in self._ids[:100]],
|
||||
channel=self._packed._to_input_channel(), id=self._ids[:100]
|
||||
)
|
||||
)
|
||||
else:
|
||||
result = await self._client(
|
||||
functions.messages.get_messages(
|
||||
id=[types.InputMessageId(id=id) for id in self._ids[:100]]
|
||||
)
|
||||
functions.messages.get_messages(id=self._ids[:100])
|
||||
)
|
||||
|
||||
self._extend_buffer(self._client, result)
|
||||
|
|
|
@ -20,7 +20,7 @@ class Event(metaclass=NoPublicConstructor):
|
|||
"""
|
||||
The :class:`~telethon.Client` that received this update.
|
||||
"""
|
||||
return self._client
|
||||
return getattr(self, "_client") # type: ignore [no-any-return]
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Literal, Optional, Union
|
||||
from typing import TYPE_CHECKING, Literal, Optional, Tuple, Union
|
||||
|
||||
from ..event import Event
|
||||
|
||||
|
@ -144,7 +144,7 @@ class TextOnly:
|
|||
"""
|
||||
|
||||
|
||||
MediaTypes = Union[Literal["photo"], Literal["audio"], Literal["video"]]
|
||||
MediaType = Union[Literal["photo"], Literal["audio"], Literal["video"]]
|
||||
|
||||
|
||||
class Media:
|
||||
|
@ -166,15 +166,15 @@ class Media:
|
|||
|
||||
__slots__ = "_types"
|
||||
|
||||
def __init__(self, types: Optional[MediaTypes] = None) -> None:
|
||||
self._types = types
|
||||
def __init__(self, *types: MediaType) -> None:
|
||||
self._types = types or None
|
||||
|
||||
@property
|
||||
def types(self) -> MediaTypes:
|
||||
def types(self) -> Tuple[MediaType, ...]:
|
||||
"""
|
||||
The media types being checked.
|
||||
"""
|
||||
return self._types
|
||||
return self._types or ()
|
||||
|
||||
def __call__(self, event: Event) -> bool:
|
||||
if self._types is None:
|
||||
|
|
|
@ -41,7 +41,7 @@ class AsyncList(abc.ABC, Generic[T]):
|
|||
|
||||
async def _collect(self) -> List[T]:
|
||||
prev = -1
|
||||
while prev != len(self._buffer):
|
||||
while not self._done and prev != len(self._buffer):
|
||||
prev = len(self._buffer)
|
||||
await self._fetch_next()
|
||||
return list(self._buffer)
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import mimetypes
|
||||
import os
|
||||
from inspect import isawaitable
|
||||
from io import BufferedReader, BufferedWriter
|
||||
from mimetypes import guess_type
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import Any, Coroutine, List, Optional, Protocol, Self, Type, Union
|
||||
from typing import TYPE_CHECKING, Any, Coroutine, List, Optional, Protocol, Self, Union
|
||||
|
||||
from ...tl import abcs, types
|
||||
from .meta import NoPublicConstructor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..client import Client
|
||||
|
||||
math_round = round
|
||||
|
||||
|
||||
|
@ -24,10 +29,43 @@ def photo_size_byte_count(size: abcs.PhotoSize) -> int:
|
|||
elif isinstance(size, types.PhotoSizeProgressive):
|
||||
return max(size.sizes)
|
||||
elif isinstance(size, types.PhotoStrippedSize):
|
||||
if len(size.bytes) < 3 or size.bytes[0] != 1:
|
||||
return len(size.bytes)
|
||||
return (
|
||||
len(stripped_size_header)
|
||||
+ (len(size.bytes) - 3)
|
||||
+ len(stripped_size_footer)
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("unexpected case")
|
||||
|
||||
return len(size.bytes) + 622
|
||||
|
||||
stripped_size_header = bytes.fromhex(
|
||||
"FFD8FFE000104A46494600010100000100010000FFDB004300281C1E231E19282321232D2B28303C64413C37373C7B585D4964918099968F808C8AA0B4E6C3A0AADAAD8A8CC8FFCBDAEEF5FFFFFF9BC1FFFFFFFAFFE6FDFFF8FFDB0043012B2D2D3C353C76414176F8A58CA5F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8FFC0001108001E002803012200021101031101FFC4001F0000010501010101010100000000000000000102030405060708090A0BFFC400B5100002010303020403050504040000017D01020300041105122131410613516107227114328191A1082342B1C11552D1F02433627282090A161718191A25262728292A3435363738393A434445464748494A535455565758595A636465666768696A737475767778797A838485868788898A92939495969798999AA2A3A4A5A6A7A8A9AAB2B3B4B5B6B7B8B9BAC2C3C4C5C6C7C8C9CAD2D3D4D5D6D7D8D9DAE1E2E3E4E5E6E7E8E9EAF1F2F3F4F5F6F7F8F9FAFFC4001F0100030101010101010101010000000000000102030405060708090A0BFFC400B51100020102040403040705040400010277000102031104052131061241510761711322328108144291A1B1C109233352F0156272D10A162434E125F11718191A262728292A35363738393A434445464748494A535455565758595A636465666768696A737475767778797A82838485868788898A92939495969798999AA2A3A4A5A6A7A8A9AAB2B3B4B5B6B7B8B9BAC2C3C4C5C6C7C8C9CAD2D3D4D5D6D7D8D9DAE2E3E4E5E6E7E8E9EAF2F3F4F5F6F7F8F9FAFFDA000C03010002110311003F00"
|
||||
)
|
||||
stripped_size_footer = bytes.fromhex("FFD9")
|
||||
|
||||
|
||||
def expand_stripped_size(data: bytes) -> bytearray:
|
||||
header = bytearray(stripped_size_header)
|
||||
header[164] = data[1]
|
||||
header[166] = data[2]
|
||||
return bytes(header) + data[3:] + stripped_size_footer
|
||||
|
||||
|
||||
def photo_size_dimensions(
|
||||
size: abcs.PhotoSize,
|
||||
) -> Optional[types.DocumentAttributeImageSize]:
|
||||
if isinstance(size, types.PhotoCachedSize):
|
||||
return types.DocumentAttributeImageSize(w=size.w, h=size.h)
|
||||
elif isinstance(size, types.PhotoPathSize):
|
||||
return None
|
||||
elif isinstance(size, types.PhotoSize):
|
||||
return types.DocumentAttributeImageSize(w=size.w, h=size.h)
|
||||
elif isinstance(size, types.PhotoSizeEmpty):
|
||||
return None
|
||||
elif isinstance(size, types.PhotoSizeProgressive):
|
||||
return types.DocumentAttributeImageSize(w=size.w, h=size.h)
|
||||
elif isinstance(size, types.PhotoStrippedSize):
|
||||
return types.DocumentAttributeImageSize(w=size.bytes[1], h=size.bytes[2])
|
||||
else:
|
||||
raise RuntimeError("unexpected case")
|
||||
|
||||
|
@ -122,7 +160,10 @@ class File(metaclass=NoPublicConstructor):
|
|||
photo: bool,
|
||||
muted: bool,
|
||||
input_media: Optional[abcs.InputMedia],
|
||||
raw: Optional[Union[types.MessageMediaDocument, types.MessageMediaPhoto]],
|
||||
thumb: Optional[abcs.PhotoSize],
|
||||
thumbs: Optional[List[abcs.PhotoSize]],
|
||||
raw: Optional[Union[abcs.MessageMedia, abcs.Photo, abcs.Document]],
|
||||
client: Optional[Client],
|
||||
):
|
||||
self._path = path
|
||||
self._file = file
|
||||
|
@ -132,53 +173,95 @@ class File(metaclass=NoPublicConstructor):
|
|||
self._mime = mime
|
||||
self._photo = photo
|
||||
self._muted = muted
|
||||
self._input_file: Optional[abcs.InputFile] = None
|
||||
self._input_media: Optional[abcs.InputMedia] = input_media
|
||||
self._input_media = input_media
|
||||
self._thumb = thumb
|
||||
self._thumbs = thumbs
|
||||
self._raw = raw
|
||||
self._client = client
|
||||
|
||||
@classmethod
|
||||
def _try_from_raw_message_media(cls, raw: abcs.MessageMedia) -> Optional[Self]:
|
||||
def _try_from_raw_message_media(
|
||||
cls, client: Client, raw: abcs.MessageMedia
|
||||
) -> Optional[Self]:
|
||||
if isinstance(raw, types.MessageMediaDocument):
|
||||
if isinstance(raw.document, types.Document):
|
||||
if raw.document:
|
||||
return cls._try_from_raw_document(
|
||||
client,
|
||||
raw.document,
|
||||
spoiler=raw.spoiler,
|
||||
ttl_seconds=raw.ttl_seconds,
|
||||
orig_raw=raw,
|
||||
)
|
||||
elif isinstance(raw, types.MessageMediaPhoto):
|
||||
if raw.photo:
|
||||
return cls._try_from_raw_photo(
|
||||
client,
|
||||
raw.photo,
|
||||
spoiler=raw.spoiler,
|
||||
ttl_seconds=raw.ttl_seconds,
|
||||
orig_raw=raw,
|
||||
)
|
||||
elif isinstance(raw, types.MessageMediaWebPage):
|
||||
if isinstance(raw.webpage, types.WebPage):
|
||||
if raw.webpage.document:
|
||||
return cls._try_from_raw_document(
|
||||
client, raw.webpage.document, orig_raw=raw
|
||||
)
|
||||
if raw.webpage.photo:
|
||||
return cls._try_from_raw_photo(
|
||||
client, raw.webpage.photo, orig_raw=raw
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _try_from_raw_document(
|
||||
cls,
|
||||
client: Client,
|
||||
raw: abcs.Document,
|
||||
*,
|
||||
spoiler: bool = False,
|
||||
ttl_seconds: Optional[int] = None,
|
||||
orig_raw: Optional[abcs.MessageMedia] = None,
|
||||
) -> Optional[Self]:
|
||||
if isinstance(raw, types.Document):
|
||||
return cls._create(
|
||||
path=None,
|
||||
file=None,
|
||||
attributes=raw.document.attributes,
|
||||
size=raw.document.size,
|
||||
attributes=raw.attributes,
|
||||
size=raw.size,
|
||||
name=next(
|
||||
(
|
||||
a.file_name
|
||||
for a in raw.document.attributes
|
||||
for a in raw.attributes
|
||||
if isinstance(a, types.DocumentAttributeFilename)
|
||||
),
|
||||
"",
|
||||
),
|
||||
mime=raw.document.mime_type,
|
||||
mime=raw.mime_type,
|
||||
photo=False,
|
||||
muted=next(
|
||||
(
|
||||
a.nosound
|
||||
for a in raw.document.attributes
|
||||
for a in raw.attributes
|
||||
if isinstance(a, types.DocumentAttributeVideo)
|
||||
),
|
||||
False,
|
||||
),
|
||||
input_media=types.InputMediaDocument(
|
||||
spoiler=raw.spoiler,
|
||||
spoiler=spoiler,
|
||||
id=types.InputDocument(
|
||||
id=raw.document.id,
|
||||
access_hash=raw.document.access_hash,
|
||||
file_reference=raw.document.file_reference,
|
||||
id=raw.id,
|
||||
access_hash=raw.access_hash,
|
||||
file_reference=raw.file_reference,
|
||||
),
|
||||
ttl_seconds=raw.ttl_seconds,
|
||||
ttl_seconds=ttl_seconds,
|
||||
query=None,
|
||||
),
|
||||
raw=raw,
|
||||
)
|
||||
elif isinstance(raw, types.MessageMediaPhoto):
|
||||
if raw.photo:
|
||||
return cls._try_from_raw_photo(
|
||||
raw.photo, spoiler=raw.spoiler, ttl_seconds=raw.ttl_seconds
|
||||
thumb=None,
|
||||
thumbs=raw.thumbs,
|
||||
raw=orig_raw or raw,
|
||||
client=client,
|
||||
)
|
||||
|
||||
return None
|
||||
|
@ -186,17 +269,20 @@ class File(metaclass=NoPublicConstructor):
|
|||
@classmethod
|
||||
def _try_from_raw_photo(
|
||||
cls,
|
||||
client: Client,
|
||||
raw: abcs.Photo,
|
||||
*,
|
||||
spoiler: bool = False,
|
||||
ttl_seconds: Optional[int] = None,
|
||||
orig_raw: Optional[abcs.MessageMedia] = None,
|
||||
) -> Optional[Self]:
|
||||
if isinstance(raw, types.Photo):
|
||||
largest_thumb = max(raw.sizes, key=photo_size_byte_count)
|
||||
return cls._create(
|
||||
path=None,
|
||||
file=None,
|
||||
attributes=[],
|
||||
size=max(map(photo_size_byte_count, raw.sizes)),
|
||||
size=photo_size_byte_count(largest_thumb),
|
||||
name="",
|
||||
mime="image/jpeg",
|
||||
photo=True,
|
||||
|
@ -210,9 +296,10 @@ class File(metaclass=NoPublicConstructor):
|
|||
),
|
||||
ttl_seconds=ttl_seconds,
|
||||
),
|
||||
raw=types.MessageMediaPhoto(
|
||||
spoiler=spoiler, photo=raw, ttl_seconds=ttl_seconds
|
||||
),
|
||||
thumb=largest_thumb,
|
||||
thumbs=[t for t in raw.sizes if t is not largest_thumb],
|
||||
raw=orig_raw or raw,
|
||||
client=client,
|
||||
)
|
||||
|
||||
return None
|
||||
|
@ -358,12 +445,100 @@ class File(metaclass=NoPublicConstructor):
|
|||
photo=photo,
|
||||
muted=muted,
|
||||
input_media=input_media,
|
||||
thumb=None,
|
||||
thumbs=None,
|
||||
raw=None,
|
||||
client=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def ext(self) -> str:
|
||||
return self._path.suffix if self._path else ""
|
||||
"""
|
||||
The file extension, including the leading dot ``.``.
|
||||
|
||||
If the file does not represent and local file, the mimetype is used in :meth:`mimetypes.guess_extension`.
|
||||
|
||||
If no extension is known for the mimetype, the empty string will be returned.
|
||||
This makes it safe to always append this property to a file name.
|
||||
"""
|
||||
if self._path:
|
||||
return self._path.suffix
|
||||
else:
|
||||
return mimetypes.guess_extension(self._mime) or ""
|
||||
|
||||
@property
|
||||
def thumbnails(self) -> List[File]:
|
||||
"""
|
||||
The file thumbnails.
|
||||
|
||||
For photos, these are often downscaled versions of the original size.
|
||||
|
||||
For documents, these will be the thumbnails present in the document.
|
||||
"""
|
||||
return [
|
||||
File._create(
|
||||
path=None,
|
||||
file=None,
|
||||
attributes=[],
|
||||
size=photo_size_byte_count(t),
|
||||
name="",
|
||||
mime="image/jpeg",
|
||||
photo=True,
|
||||
muted=False,
|
||||
input_media=self._input_media,
|
||||
thumb=t,
|
||||
thumbs=None,
|
||||
raw=self._raw,
|
||||
client=self._client,
|
||||
)
|
||||
for t in (self._thumbs or [])
|
||||
]
|
||||
|
||||
@property
|
||||
def width(self) -> Optional[int]:
|
||||
"""
|
||||
The width of the image or video, if available.
|
||||
"""
|
||||
if self._thumb and (dim := photo_size_dimensions(self._thumb)):
|
||||
return dim.w
|
||||
|
||||
for attr in self._attributes:
|
||||
if isinstance(
|
||||
attr, (types.DocumentAttributeImageSize, types.DocumentAttributeVideo)
|
||||
):
|
||||
return attr.w
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def height(self) -> Optional[int]:
|
||||
"""
|
||||
The width of the image or video, if available.
|
||||
"""
|
||||
if self._thumb and (dim := photo_size_dimensions(self._thumb)):
|
||||
return dim.h
|
||||
|
||||
for attr in self._attributes:
|
||||
if isinstance(
|
||||
attr, (types.DocumentAttributeImageSize, types.DocumentAttributeVideo)
|
||||
):
|
||||
return attr.h
|
||||
|
||||
return None
|
||||
|
||||
async def download(self, file: Union[str, Path, OutFileLike]) -> None:
|
||||
"""
|
||||
Alias for :meth:`telethon.Client.download`.
|
||||
|
||||
The file must have been obtained from Telegram to be downloadable.
|
||||
This means you cannot create local files, or files with an URL, and download them.
|
||||
|
||||
See the documentation of :meth:`~telethon.Client.download` for an explanation of the parameters.
|
||||
"""
|
||||
if not self._client:
|
||||
raise ValueError("only files from Telegram can be downloaded")
|
||||
|
||||
await self._client.download(self, file)
|
||||
|
||||
def _open(self) -> InWrapper:
|
||||
file = self._file or self._path
|
||||
|
@ -372,27 +547,33 @@ class File(metaclass=NoPublicConstructor):
|
|||
return InWrapper(file)
|
||||
|
||||
def _input_location(self) -> abcs.InputFileLocation:
|
||||
thumb_types = (
|
||||
types.PhotoSizeEmpty,
|
||||
types.PhotoSize,
|
||||
types.PhotoCachedSize,
|
||||
types.PhotoStrippedSize,
|
||||
types.PhotoSizeProgressive,
|
||||
types.PhotoPathSize,
|
||||
)
|
||||
if isinstance(self._input_media, types.InputMediaDocument):
|
||||
assert isinstance(self._input_media.id, types.InputDocument)
|
||||
return types.InputDocumentFileLocation(
|
||||
id=self._input_media.id.id,
|
||||
access_hash=self._input_media.id.access_hash,
|
||||
file_reference=self._input_media.id.file_reference,
|
||||
thumb_size="",
|
||||
thumb_size=self._thumb.type
|
||||
if isinstance(self._thumb, thumb_types)
|
||||
else "",
|
||||
)
|
||||
elif isinstance(self._input_media, types.InputMediaPhoto):
|
||||
assert isinstance(self._input_media.id, types.InputPhoto)
|
||||
assert isinstance(self._raw, types.MessageMediaPhoto)
|
||||
assert isinstance(self._raw.photo, types.Photo)
|
||||
|
||||
size = max(self._raw.photo.sizes, key=photo_size_byte_count)
|
||||
assert hasattr(size, "type")
|
||||
assert isinstance(self._thumb, thumb_types)
|
||||
|
||||
return types.InputPhotoFileLocation(
|
||||
id=self._input_media.id.id,
|
||||
access_hash=self._input_media.id.access_hash,
|
||||
file_reference=self._input_media.id.file_reference,
|
||||
thumb_size=size.type,
|
||||
thumb_size=self._thumb.type,
|
||||
)
|
||||
else:
|
||||
raise TypeError(f"cannot use file for downloading: {self}")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Self
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Self, Union
|
||||
|
||||
from ...tl import abcs, types
|
||||
from ..parsers import generate_html_message, generate_markdown_message
|
||||
|
@ -109,7 +109,7 @@ class Message(metaclass=NoPublicConstructor):
|
|||
|
||||
def _file(self) -> Optional[File]:
|
||||
return (
|
||||
File._try_from_raw_message_media(self._raw.media)
|
||||
File._try_from_raw_message_media(self._client, self._raw.media)
|
||||
if isinstance(self._raw, types.Message) and self._raw.media
|
||||
else None
|
||||
)
|
||||
|
@ -147,13 +147,80 @@ class Message(metaclass=NoPublicConstructor):
|
|||
def file(self) -> Optional[File]:
|
||||
return self._file()
|
||||
|
||||
async def delete(self, *, revoke: bool = True) -> int:
|
||||
@property
|
||||
def replied_message_id(self) -> Optional[int]:
|
||||
"""
|
||||
Get the message identifier of the replied message.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`get_reply_message`
|
||||
"""
|
||||
if header := getattr(self._raw, "reply_to", None):
|
||||
return getattr(header, "reply_to_msg_id", None)
|
||||
|
||||
return None
|
||||
|
||||
async def get_reply_message(self) -> Optional[Message]:
|
||||
"""
|
||||
Alias for :meth:`telethon.Client.get_messages_with_ids`.
|
||||
|
||||
If all you want is to check whether this message is a reply, use :attr:`replied_message_id`.
|
||||
"""
|
||||
if self.replied_message_id is not None:
|
||||
from ..client.messages import CherryPickedList
|
||||
|
||||
lst = CherryPickedList(self._client, self.chat, [])
|
||||
lst._ids.append(types.InputMessageReplyTo(id=self.id))
|
||||
return (await lst)[0]
|
||||
return None
|
||||
|
||||
async def respond(
|
||||
self,
|
||||
text: Optional[Union[str, Message]] = None,
|
||||
*,
|
||||
markdown: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
link_preview: bool = False,
|
||||
) -> Message:
|
||||
"""
|
||||
Alias for :meth:`telethon.Client.send_message`.
|
||||
|
||||
See the documentation of :meth:`~telethon.Client.send_message` for an explanation of the parameters.
|
||||
"""
|
||||
return await self._client.send_message(
|
||||
self.chat, text, markdown=markdown, html=html, link_preview=link_preview
|
||||
)
|
||||
|
||||
async def reply(
|
||||
self,
|
||||
text: Optional[Union[str, Message]] = None,
|
||||
*,
|
||||
markdown: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
link_preview: bool = False,
|
||||
) -> Message:
|
||||
"""
|
||||
Alias for :meth:`telethon.Client.send_message` with the ``reply_to`` parameter set to this message.
|
||||
|
||||
See the documentation of :meth:`~telethon.Client.send_message` for an explanation of the parameters.
|
||||
"""
|
||||
return await self._client.send_message(
|
||||
self.chat,
|
||||
text,
|
||||
markdown=markdown,
|
||||
html=html,
|
||||
link_preview=link_preview,
|
||||
reply_to=self.id,
|
||||
)
|
||||
|
||||
async def delete(self, *, revoke: bool = True) -> None:
|
||||
"""
|
||||
Alias for :meth:`telethon.Client.delete_messages`.
|
||||
|
||||
See the documentation of :meth:`~telethon.Client.delete_messages` for an explanation of the parameters.
|
||||
"""
|
||||
return await self._client.delete_messages(self.chat, [self.id], revoke=revoke)
|
||||
await self._client.delete_messages(self.chat, [self.id], revoke=revoke)
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
|
@ -176,10 +243,92 @@ class Message(metaclass=NoPublicConstructor):
|
|||
link_preview=link_preview,
|
||||
)
|
||||
|
||||
async def forward_to(self, target: ChatLike) -> Message:
|
||||
async def forward(self, target: ChatLike) -> Message:
|
||||
"""
|
||||
Alias for :meth:`telethon.Client.forward_messages`.
|
||||
|
||||
See the documentation of :meth:`~telethon.Client.forward_messages` for an explanation of the parameters.
|
||||
"""
|
||||
return (await self._client.forward_messages(target, [self.id], self.chat))[0]
|
||||
|
||||
async def mark_read(self) -> None:
|
||||
pass
|
||||
|
||||
async def pin(self, *, notify: bool = False, pm_oneside: bool = False) -> None:
|
||||
pass
|
||||
|
||||
async def unpin(self) -> None:
|
||||
pass
|
||||
|
||||
# ---
|
||||
|
||||
@property
|
||||
def forward_info(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def buttons(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def web_preview(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def voice(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def video_note(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def gif(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def sticker(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def contact(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def game(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def geo(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def invoice(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def poll(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def venue(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def dice(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def via_bot(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def silent(self) -> bool:
|
||||
return getattr(self._raw, "silent", None) or False
|
||||
|
||||
@property
|
||||
def can_forward(self) -> bool:
|
||||
if isinstance(self._raw, types.Message):
|
||||
return not self._raw.noforwards
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -134,7 +134,8 @@ class SqliteSession(Storage):
|
|||
)
|
||||
if user := session.user:
|
||||
c.execute(
|
||||
"insert into user values (?, ?, ?)", (user.id, user.dc, int(user.bot))
|
||||
"insert into user values (?, ?, ?, ?)",
|
||||
(user.id, user.dc, int(user.bot), user.username),
|
||||
)
|
||||
if state := session.state:
|
||||
c.execute(
|
||||
|
@ -193,7 +194,8 @@ class SqliteSession(Storage):
|
|||
create table user(
|
||||
id integer primary key,
|
||||
dc integer not null,
|
||||
bot integer not null
|
||||
bot integer not null,
|
||||
username text
|
||||
);
|
||||
create table state(
|
||||
pts integer not null,
|
||||
|
|
Loading…
Reference in New Issue
Block a user