Continue implementing file and message

This commit is contained in:
Lonami Exo 2023-10-02 23:26:40 +02:00
parent 4df1f4537b
commit 25a2b53d3f
12 changed files with 514 additions and 102 deletions

View File

@ -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

View File

@ -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
----------------------------------

View File

@ -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)

View File

@ -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(

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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}")

View File

@ -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

View File

@ -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,