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

@@ -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,71 +173,116 @@ 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):
return cls._create(
path=None,
file=None,
attributes=raw.document.attributes,
size=raw.document.size,
name=next(
(
a.file_name
for a in raw.document.attributes
if isinstance(a, types.DocumentAttributeFilename)
),
"",
),
mime=raw.document.mime_type,
photo=False,
muted=next(
(
a.nosound
for a in raw.document.attributes
if isinstance(a, types.DocumentAttributeVideo)
),
False,
),
input_media=types.InputMediaDocument(
spoiler=raw.spoiler,
id=types.InputDocument(
id=raw.document.id,
access_hash=raw.document.access_hash,
file_reference=raw.document.file_reference,
),
ttl_seconds=raw.ttl_seconds,
query=None,
),
raw=raw,
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(
raw.photo, spoiler=raw.spoiler, ttl_seconds=raw.ttl_seconds
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.attributes,
size=raw.size,
name=next(
(
a.file_name
for a in raw.attributes
if isinstance(a, types.DocumentAttributeFilename)
),
"",
),
mime=raw.mime_type,
photo=False,
muted=next(
(
a.nosound
for a in raw.attributes
if isinstance(a, types.DocumentAttributeVideo)
),
False,
),
input_media=types.InputMediaDocument(
spoiler=spoiler,
id=types.InputDocument(
id=raw.id,
access_hash=raw.access_hash,
file_reference=raw.file_reference,
),
ttl_seconds=ttl_seconds,
query=None,
),
thumb=None,
thumbs=raw.thumbs,
raw=orig_raw or raw,
client=client,
)
return None
@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,