mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-25 10:53:44 +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:
|
Using one of the examples from their v3 documentation with logging and comments removed:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher, types
|
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:
|
In Telethon:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import asyncio, html
|
import asyncio, html
|
||||||
|
|
||||||
from telethon import Client, RpcError, types, events
|
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.
|
* ``tl.types`` contains concrete instances, the "bare" types Telegram actually returns.
|
||||||
You'll probably use these with :func:`isinstance` a lot.
|
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
|
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.
|
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`,
|
Instead, you can access the :attr:`types.Message.text`,
|
||||||
:attr:`~types.Message.text_markdown` or :attr:`~types.Message.text_html`.
|
: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.
|
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.
|
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
|
Event and filters are now separate
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
|
@ -158,18 +158,20 @@ class ProfilePhotoList(AsyncList[File]):
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(result, types.photos.Photos):
|
if isinstance(result, types.photos.Photos):
|
||||||
self._buffer.extend(
|
photos = result.photos
|
||||||
filter(None, (File._try_from_raw_photo(p) for p in result.photos))
|
|
||||||
)
|
|
||||||
self._total = len(result.photos)
|
self._total = len(result.photos)
|
||||||
elif isinstance(result, types.photos.PhotosSlice):
|
elif isinstance(result, types.photos.PhotosSlice):
|
||||||
self._buffer.extend(
|
photos = result.photos
|
||||||
filter(None, (File._try_from_raw_photo(p) for p in result.photos))
|
|
||||||
)
|
|
||||||
self._total = result.count
|
self._total = result.count
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("unexpected case")
|
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]:
|
def get_profile_photos(self: Client, chat: ChatLike) -> AsyncList[File]:
|
||||||
return ProfilePhotoList(self, chat)
|
return ProfilePhotoList(self, chat)
|
||||||
|
|
|
@ -1235,7 +1235,8 @@ class Client:
|
||||||
*,
|
*,
|
||||||
markdown: Optional[str] = None,
|
markdown: Optional[str] = None,
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
link_preview: Optional[bool] = None,
|
link_preview: bool = False,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send a message.
|
Send a message.
|
||||||
|
@ -1254,6 +1255,17 @@ class Client:
|
||||||
:param text_html:
|
:param text_html:
|
||||||
Message text, parsed as 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.
|
Note that exactly one *text* parameter must be provided.
|
||||||
|
|
||||||
See the section on :doc:`/concepts/messages` to learn about message formatting.
|
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!**')
|
await client.send_message(chat, markdown='**Hello!**')
|
||||||
"""
|
"""
|
||||||
return await send_message(
|
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(
|
async def send_photo(
|
||||||
|
|
|
@ -15,6 +15,7 @@ from ..types import (
|
||||||
OutFileLike,
|
OutFileLike,
|
||||||
OutWrapper,
|
OutWrapper,
|
||||||
)
|
)
|
||||||
|
from ..types.file import expand_stripped_size
|
||||||
from ..utils import generate_random_id
|
from ..utils import generate_random_id
|
||||||
from .messages import parse_message
|
from .messages import parse_message
|
||||||
|
|
||||||
|
@ -182,8 +183,9 @@ async def send_file(
|
||||||
muted=muted,
|
muted=muted,
|
||||||
)
|
)
|
||||||
message, entities = parse_message(
|
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()
|
peer = (await self._resolve_to_packed(chat))._to_input_peer()
|
||||||
|
|
||||||
|
@ -312,6 +314,9 @@ class FileBytesList(AsyncList[bytes]):
|
||||||
self._client = client
|
self._client = client
|
||||||
self._loc = file._input_location()
|
self._loc = file._input_location()
|
||||||
self._offset = 0
|
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:
|
async def _fetch_next(self) -> None:
|
||||||
result = await self._client(
|
result = await self._client(
|
||||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import sys
|
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 ...session import PackedChat
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
|
@ -16,11 +16,15 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
def parse_message(
|
def parse_message(
|
||||||
*,
|
*,
|
||||||
text: Optional[str] = None,
|
text: Optional[Union[str, Message]],
|
||||||
markdown: Optional[str] = None,
|
markdown: Optional[str],
|
||||||
html: Optional[str] = None,
|
html: Optional[str],
|
||||||
) -> Tuple[str, Optional[List[abcs.MessageEntity]]]:
|
allow_empty: bool,
|
||||||
if sum((text is not None, markdown is not None, html is not None)) != 1:
|
) -> 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")
|
raise ValueError("must specify exactly one of text, markdown or html")
|
||||||
|
|
||||||
if text is not None:
|
if text is not None:
|
||||||
|
@ -38,14 +42,17 @@ def parse_message(
|
||||||
async def send_message(
|
async def send_message(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
chat: ChatLike,
|
||||||
text: Optional[str] = None,
|
text: Optional[Union[str, Message]] = None,
|
||||||
*,
|
*,
|
||||||
markdown: Optional[str] = None,
|
markdown: Optional[str] = None,
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
link_preview: Optional[bool] = None,
|
link_preview: Optional[bool] = None,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
peer = (await self._resolve_to_packed(chat))._to_input_peer()
|
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()
|
random_id = generate_random_id()
|
||||||
return self._build_message_map(
|
return self._build_message_map(
|
||||||
await self(
|
await self(
|
||||||
|
@ -57,7 +64,11 @@ async def send_message(
|
||||||
noforwards=False,
|
noforwards=False,
|
||||||
update_stickersets_order=False,
|
update_stickersets_order=False,
|
||||||
peer=peer,
|
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,
|
message=message,
|
||||||
random_id=random_id,
|
random_id=random_id,
|
||||||
reply_markup=None,
|
reply_markup=None,
|
||||||
|
@ -65,6 +76,27 @@ async def send_message(
|
||||||
schedule_date=None,
|
schedule_date=None,
|
||||||
send_as=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,
|
peer,
|
||||||
).with_random_id(random_id)
|
).with_random_id(random_id)
|
||||||
|
@ -81,7 +113,10 @@ async def edit_message(
|
||||||
link_preview: Optional[bool] = None,
|
link_preview: Optional[bool] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
peer = (await self._resolve_to_packed(chat))._to_input_peer()
|
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(
|
return self._build_message_map(
|
||||||
await self(
|
await self(
|
||||||
functions.messages.edit_message(
|
functions.messages.edit_message(
|
||||||
|
@ -232,8 +267,8 @@ class HistoryList(MessageList):
|
||||||
|
|
||||||
self._extend_buffer(self._client, result)
|
self._extend_buffer(self._client, result)
|
||||||
self._limit -= len(self._buffer)
|
self._limit -= len(self._buffer)
|
||||||
self._done = not self._limit
|
self._done |= not self._limit
|
||||||
if self._buffer:
|
if self._buffer and not self._done:
|
||||||
last = self._last_non_empty_message()
|
last = self._last_non_empty_message()
|
||||||
self._offset_id = self._buffer[-1].id
|
self._offset_id = self._buffer[-1].id
|
||||||
if (date := getattr(last, "date", None)) is not None:
|
if (date := getattr(last, "date", None)) is not None:
|
||||||
|
@ -268,7 +303,7 @@ class CherryPickedList(MessageList):
|
||||||
self._client = client
|
self._client = client
|
||||||
self._chat = chat
|
self._chat = chat
|
||||||
self._packed: Optional[PackedChat] = None
|
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:
|
async def _fetch_next(self) -> None:
|
||||||
if not self._ids:
|
if not self._ids:
|
||||||
|
@ -279,15 +314,12 @@ class CherryPickedList(MessageList):
|
||||||
if self._packed.is_channel():
|
if self._packed.is_channel():
|
||||||
result = await self._client(
|
result = await self._client(
|
||||||
functions.channels.get_messages(
|
functions.channels.get_messages(
|
||||||
channel=self._packed._to_input_channel(),
|
channel=self._packed._to_input_channel(), id=self._ids[:100]
|
||||||
id=[types.InputMessageId(id=id) for id in self._ids[:100]],
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = await self._client(
|
result = await self._client(
|
||||||
functions.messages.get_messages(
|
functions.messages.get_messages(id=self._ids[:100])
|
||||||
id=[types.InputMessageId(id=id) for id in self._ids[:100]]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._extend_buffer(self._client, result)
|
self._extend_buffer(self._client, result)
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Event(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
The :class:`~telethon.Client` that received this update.
|
The :class:`~telethon.Client` that received this update.
|
||||||
"""
|
"""
|
||||||
return self._client
|
return getattr(self, "_client") # type: ignore [no-any-return]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, Literal, Optional, Union
|
from typing import TYPE_CHECKING, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
from ..event import Event
|
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:
|
class Media:
|
||||||
|
@ -166,15 +166,15 @@ class Media:
|
||||||
|
|
||||||
__slots__ = "_types"
|
__slots__ = "_types"
|
||||||
|
|
||||||
def __init__(self, types: Optional[MediaTypes] = None) -> None:
|
def __init__(self, *types: MediaType) -> None:
|
||||||
self._types = types
|
self._types = types or None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def types(self) -> MediaTypes:
|
def types(self) -> Tuple[MediaType, ...]:
|
||||||
"""
|
"""
|
||||||
The media types being checked.
|
The media types being checked.
|
||||||
"""
|
"""
|
||||||
return self._types
|
return self._types or ()
|
||||||
|
|
||||||
def __call__(self, event: Event) -> bool:
|
def __call__(self, event: Event) -> bool:
|
||||||
if self._types is None:
|
if self._types is None:
|
||||||
|
|
|
@ -41,7 +41,7 @@ class AsyncList(abc.ABC, Generic[T]):
|
||||||
|
|
||||||
async def _collect(self) -> List[T]:
|
async def _collect(self) -> List[T]:
|
||||||
prev = -1
|
prev = -1
|
||||||
while prev != len(self._buffer):
|
while not self._done and prev != len(self._buffer):
|
||||||
prev = len(self._buffer)
|
prev = len(self._buffer)
|
||||||
await self._fetch_next()
|
await self._fetch_next()
|
||||||
return list(self._buffer)
|
return list(self._buffer)
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from io import BufferedReader, BufferedWriter
|
from io import BufferedReader, BufferedWriter
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import TracebackType
|
from typing import TYPE_CHECKING, Any, Coroutine, List, Optional, Protocol, Self, Union
|
||||||
from typing import Any, Coroutine, List, Optional, Protocol, Self, Type, Union
|
|
||||||
|
|
||||||
from ...tl import abcs, types
|
from ...tl import abcs, types
|
||||||
from .meta import NoPublicConstructor
|
from .meta import NoPublicConstructor
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..client import Client
|
||||||
|
|
||||||
math_round = round
|
math_round = round
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,10 +29,43 @@ def photo_size_byte_count(size: abcs.PhotoSize) -> int:
|
||||||
elif isinstance(size, types.PhotoSizeProgressive):
|
elif isinstance(size, types.PhotoSizeProgressive):
|
||||||
return max(size.sizes)
|
return max(size.sizes)
|
||||||
elif isinstance(size, types.PhotoStrippedSize):
|
elif isinstance(size, types.PhotoStrippedSize):
|
||||||
if len(size.bytes) < 3 or size.bytes[0] != 1:
|
return (
|
||||||
return len(size.bytes)
|
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:
|
else:
|
||||||
raise RuntimeError("unexpected case")
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
@ -122,7 +160,10 @@ class File(metaclass=NoPublicConstructor):
|
||||||
photo: bool,
|
photo: bool,
|
||||||
muted: bool,
|
muted: bool,
|
||||||
input_media: Optional[abcs.InputMedia],
|
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._path = path
|
||||||
self._file = file
|
self._file = file
|
||||||
|
@ -132,71 +173,116 @@ class File(metaclass=NoPublicConstructor):
|
||||||
self._mime = mime
|
self._mime = mime
|
||||||
self._photo = photo
|
self._photo = photo
|
||||||
self._muted = muted
|
self._muted = muted
|
||||||
self._input_file: Optional[abcs.InputFile] = None
|
self._input_media = input_media
|
||||||
self._input_media: Optional[abcs.InputMedia] = input_media
|
self._thumb = thumb
|
||||||
|
self._thumbs = thumbs
|
||||||
self._raw = raw
|
self._raw = raw
|
||||||
|
self._client = client
|
||||||
|
|
||||||
@classmethod
|
@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, types.MessageMediaDocument):
|
||||||
if isinstance(raw.document, types.Document):
|
if raw.document:
|
||||||
return cls._create(
|
return cls._try_from_raw_document(
|
||||||
path=None,
|
client,
|
||||||
file=None,
|
raw.document,
|
||||||
attributes=raw.document.attributes,
|
spoiler=raw.spoiler,
|
||||||
size=raw.document.size,
|
ttl_seconds=raw.ttl_seconds,
|
||||||
name=next(
|
orig_raw=raw,
|
||||||
(
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
elif isinstance(raw, types.MessageMediaPhoto):
|
elif isinstance(raw, types.MessageMediaPhoto):
|
||||||
if raw.photo:
|
if raw.photo:
|
||||||
return cls._try_from_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
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_raw_photo(
|
def _try_from_raw_photo(
|
||||||
cls,
|
cls,
|
||||||
|
client: Client,
|
||||||
raw: abcs.Photo,
|
raw: abcs.Photo,
|
||||||
*,
|
*,
|
||||||
spoiler: bool = False,
|
spoiler: bool = False,
|
||||||
ttl_seconds: Optional[int] = None,
|
ttl_seconds: Optional[int] = None,
|
||||||
|
orig_raw: Optional[abcs.MessageMedia] = None,
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
if isinstance(raw, types.Photo):
|
if isinstance(raw, types.Photo):
|
||||||
|
largest_thumb = max(raw.sizes, key=photo_size_byte_count)
|
||||||
return cls._create(
|
return cls._create(
|
||||||
path=None,
|
path=None,
|
||||||
file=None,
|
file=None,
|
||||||
attributes=[],
|
attributes=[],
|
||||||
size=max(map(photo_size_byte_count, raw.sizes)),
|
size=photo_size_byte_count(largest_thumb),
|
||||||
name="",
|
name="",
|
||||||
mime="image/jpeg",
|
mime="image/jpeg",
|
||||||
photo=True,
|
photo=True,
|
||||||
|
@ -210,9 +296,10 @@ class File(metaclass=NoPublicConstructor):
|
||||||
),
|
),
|
||||||
ttl_seconds=ttl_seconds,
|
ttl_seconds=ttl_seconds,
|
||||||
),
|
),
|
||||||
raw=types.MessageMediaPhoto(
|
thumb=largest_thumb,
|
||||||
spoiler=spoiler, photo=raw, ttl_seconds=ttl_seconds
|
thumbs=[t for t in raw.sizes if t is not largest_thumb],
|
||||||
),
|
raw=orig_raw or raw,
|
||||||
|
client=client,
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -358,12 +445,100 @@ class File(metaclass=NoPublicConstructor):
|
||||||
photo=photo,
|
photo=photo,
|
||||||
muted=muted,
|
muted=muted,
|
||||||
input_media=input_media,
|
input_media=input_media,
|
||||||
|
thumb=None,
|
||||||
|
thumbs=None,
|
||||||
raw=None,
|
raw=None,
|
||||||
|
client=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ext(self) -> str:
|
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:
|
def _open(self) -> InWrapper:
|
||||||
file = self._file or self._path
|
file = self._file or self._path
|
||||||
|
@ -372,27 +547,33 @@ class File(metaclass=NoPublicConstructor):
|
||||||
return InWrapper(file)
|
return InWrapper(file)
|
||||||
|
|
||||||
def _input_location(self) -> abcs.InputFileLocation:
|
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):
|
if isinstance(self._input_media, types.InputMediaDocument):
|
||||||
assert isinstance(self._input_media.id, types.InputDocument)
|
assert isinstance(self._input_media.id, types.InputDocument)
|
||||||
return types.InputDocumentFileLocation(
|
return types.InputDocumentFileLocation(
|
||||||
id=self._input_media.id.id,
|
id=self._input_media.id.id,
|
||||||
access_hash=self._input_media.id.access_hash,
|
access_hash=self._input_media.id.access_hash,
|
||||||
file_reference=self._input_media.id.file_reference,
|
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):
|
elif isinstance(self._input_media, types.InputMediaPhoto):
|
||||||
assert isinstance(self._input_media.id, types.InputPhoto)
|
assert isinstance(self._input_media.id, types.InputPhoto)
|
||||||
assert isinstance(self._raw, types.MessageMediaPhoto)
|
assert isinstance(self._thumb, thumb_types)
|
||||||
assert isinstance(self._raw.photo, types.Photo)
|
|
||||||
|
|
||||||
size = max(self._raw.photo.sizes, key=photo_size_byte_count)
|
|
||||||
assert hasattr(size, "type")
|
|
||||||
|
|
||||||
return types.InputPhotoFileLocation(
|
return types.InputPhotoFileLocation(
|
||||||
id=self._input_media.id.id,
|
id=self._input_media.id.id,
|
||||||
access_hash=self._input_media.id.access_hash,
|
access_hash=self._input_media.id.access_hash,
|
||||||
file_reference=self._input_media.id.file_reference,
|
file_reference=self._input_media.id.file_reference,
|
||||||
thumb_size=size.type,
|
thumb_size=self._thumb.type,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise TypeError(f"cannot use file for downloading: {self}")
|
raise TypeError(f"cannot use file for downloading: {self}")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
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 ...tl import abcs, types
|
||||||
from ..parsers import generate_html_message, generate_markdown_message
|
from ..parsers import generate_html_message, generate_markdown_message
|
||||||
|
@ -109,7 +109,7 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
def _file(self) -> Optional[File]:
|
def _file(self) -> Optional[File]:
|
||||||
return (
|
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
|
if isinstance(self._raw, types.Message) and self._raw.media
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
@ -147,13 +147,80 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
def file(self) -> Optional[File]:
|
def file(self) -> Optional[File]:
|
||||||
return self._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`.
|
Alias for :meth:`telethon.Client.delete_messages`.
|
||||||
|
|
||||||
See the documentation of :meth:`~telethon.Client.delete_messages` for an explanation of the parameters.
|
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(
|
async def edit(
|
||||||
self,
|
self,
|
||||||
|
@ -176,10 +243,92 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
link_preview=link_preview,
|
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`.
|
Alias for :meth:`telethon.Client.forward_messages`.
|
||||||
|
|
||||||
See the documentation of :meth:`~telethon.Client.forward_messages` for an explanation of the parameters.
|
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]
|
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:
|
if user := session.user:
|
||||||
c.execute(
|
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:
|
if state := session.state:
|
||||||
c.execute(
|
c.execute(
|
||||||
|
@ -193,7 +194,8 @@ class SqliteSession(Storage):
|
||||||
create table user(
|
create table user(
|
||||||
id integer primary key,
|
id integer primary key,
|
||||||
dc integer not null,
|
dc integer not null,
|
||||||
bot integer not null
|
bot integer not null,
|
||||||
|
username text
|
||||||
);
|
);
|
||||||
create table state(
|
create table state(
|
||||||
pts integer not null,
|
pts integer not null,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user