Continue implementation and documentation

This commit is contained in:
Lonami Exo
2023-09-30 17:13:24 +02:00
parent 6a880f1ff1
commit 18895748c4
23 changed files with 913 additions and 88 deletions

View File

@@ -3,8 +3,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from ...tl import abcs, functions, types
from ..types import AsyncList, ChatLike, Participant
from ..types import AsyncList, ChatLike, File, Participant, RecentAction
from ..utils import build_chat_map
from .messages import SearchList
if TYPE_CHECKING:
from .client import Client
@@ -81,3 +82,106 @@ class ParticipantList(AsyncList[Participant]):
def get_participants(self: Client, chat: ChatLike) -> AsyncList[Participant]:
return ParticipantList(self, chat)
class RecentActionList(AsyncList[RecentAction]):
def __init__(
self,
client: Client,
chat: ChatLike,
):
super().__init__()
self._client = client
self._chat = chat
self._peer: Optional[types.InputChannel] = None
self._offset = 0
async def _fetch_next(self) -> None:
if self._peer is None:
self._peer = (
await self._client._resolve_to_packed(self._chat)
)._to_input_channel()
result = await self._client(
functions.channels.get_admin_log(
channel=self._peer,
q="",
min_id=0,
max_id=self._offset,
limit=100,
events_filter=None,
admins=[],
)
)
assert isinstance(result, types.channels.AdminLogResults)
chat_map = build_chat_map(result.users, result.chats)
self._buffer.extend(RecentAction._create(e, chat_map) for e in result.events)
self._total += len(self._buffer)
if self._buffer:
self._offset = min(e.id for e in self._buffer)
def get_admin_log(self: Client, chat: ChatLike) -> AsyncList[RecentAction]:
return RecentActionList(self, chat)
class ProfilePhotoList(AsyncList[File]):
def __init__(
self,
client: Client,
chat: ChatLike,
):
super().__init__()
self._client = client
self._chat = chat
self._peer: Optional[abcs.InputPeer] = None
self._search_iter: Optional[SearchList] = None
async def _fetch_next(self) -> None:
if self._peer is None:
self._peer = (
await self._client._resolve_to_packed(self._chat)
)._to_input_peer()
if isinstance(self._peer, types.InputPeerUser):
result = await self._client(
functions.photos.get_user_photos(
user_id=types.InputUser(
user_id=self._peer.user_id, access_hash=self._peer.access_hash
),
offset=0,
max_id=0,
limit=0,
)
)
if isinstance(result, types.photos.Photos):
self._buffer.extend(
filter(None, (File._try_from_raw_photo(p) for p in 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))
)
self._total = result.count
else:
raise RuntimeError("unexpected case")
def get_profile_photos(self: Client, chat: ChatLike) -> AsyncList[File]:
return ProfilePhotoList(self, chat)
def set_banned_rights(self: Client, chat: ChatLike, user: ChatLike) -> None:
pass
def set_admin_rights(self: Client, chat: ChatLike, user: ChatLike) -> None:
pass
def set_default_rights(self: Client, chat: ChatLike, user: ChatLike) -> None:
pass

View File

@@ -36,6 +36,7 @@ from ..types import (
Chat,
ChatLike,
Dialog,
Draft,
File,
InFileLike,
LoginToken,
@@ -43,6 +44,7 @@ from ..types import (
OutFileLike,
Participant,
PasswordToken,
RecentAction,
User,
)
from .auth import (
@@ -55,8 +57,15 @@ from .auth import (
sign_out,
)
from .bots import InlineResult, inline_query
from .chats import get_participants
from .dialogs import delete_dialog, get_dialogs
from .chats import (
get_admin_log,
get_participants,
get_profile_photos,
set_admin_rights,
set_banned_rights,
set_default_rights,
)
from .dialogs import delete_dialog, get_dialogs, get_drafts
from .files import (
download,
get_file_bytes,
@@ -198,7 +207,7 @@ class Client:
if self._config.catch_up and self._config.session.state:
self._message_box.load(self._config.session.state)
# ---
# Begin partially @generated
def add_event_handler(
self,
@@ -511,6 +520,29 @@ class Client:
"""
return await forward_messages(self, target, message_ids, source)
def get_admin_log(self, chat: ChatLike) -> AsyncList[RecentAction]:
"""
Get the recent actions from the administrator's log.
This method requires you to be an administrator in the :term:`chat`.
The returned actions are also known as "admin log events".
:param chat:
The :term:`chat` to fetch recent actions from.
:return: The recent actions.
.. rubric:: Example
.. code-block:: python
async for admin_log_event in client.get_admin_log(chat):
if message := admin_log_event.deleted_message:
print('Deleted:', message.text)
"""
return get_admin_log(self, chat)
def get_contacts(self) -> AsyncList[User]:
"""
Get the users in your contact list.
@@ -548,6 +580,47 @@ class Client:
"""
return get_dialogs(self)
def get_drafts(self) -> AsyncList[Draft]:
"""
Get all message drafts saved in any dialog.
:return: The existing message drafts.
.. rubric:: Example
.. code-block:: python
async for draft in client.get_drafts():
await draft.delete()
"""
return get_drafts(self)
def get_file_bytes(self, media: File) -> AsyncList[bytes]:
"""
Get the contents of an uploaded media file as chunks of :class:`bytes`.
This lets you iterate over the chunks of a file and print progress while the download occurs.
If you just want to download a file to disk without printing progress, use :meth:`download` instead.
:param media:
The media file to download.
This will often come from :attr:`telethon.types.Message.file`.
.. rubric:: Example
.. code-block:: python
if file := message.file:
with open(f'media{file.ext}', 'wb') as fd:
downloaded = 0
async for chunk in client.get_file_bytes(file):
downloaded += len(chunk)
fd.write(chunk)
print(f'Downloaded {downloaded // 1024}/{file.size // 1024} KiB')
"""
return get_file_bytes(self, media)
def get_handler_filter(
self, handler: Callable[[Event], Awaitable[Any]]
) -> Optional[Filter]:
@@ -666,6 +739,23 @@ class Client:
"""
return get_participants(self, chat)
def get_profile_photos(self, chat: ChatLike) -> AsyncList[File]:
"""
Get the profile pictures set in a chat, or user avatars.
:return: The photo files.
.. rubric:: Example
.. code-block:: python
i = 0
async for photo in client.get_profile_photos(chat):
await client.download(photo, f'{i}.jpg')
i += 1
"""
return get_profile_photos(self, chat)
async def inline_query(
self, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
) -> AsyncIterator[InlineResult]:
@@ -730,34 +820,15 @@ class Client:
Check whether the client instance is authorized (i.e. logged-in).
:return: :data:`True` if the client instance has signed-in.
"""
return await is_authorized(self)
def get_file_bytes(self, media: File) -> AsyncList[bytes]:
"""
Get the contents of an uploaded media file as chunks of :class:`bytes`.
This lets you iterate over the chunks of a file and print progress while the download occurs.
If you just want to download a file to disk without printing progress, use :meth:`download` instead.
:param media:
The media file to download.
This will often come from :attr:`telethon.types.Message.file`.
.. rubric:: Example
.. code-block:: python
if file := message.file:
with open(f'media{file.ext}', 'wb') as fd:
downloaded = 0
async for chunk in client.get_file_bytes(file):
downloaded += len(chunk)
fd.write(chunk)
print(f'Downloaded {downloaded // 1024}/{file.size // 1024} KiB')
if not await client.is_authorized():
... # need to sign in
"""
return get_file_bytes(self, media)
return await is_authorized(self)
def on(
self, event_cls: Type[Event], filter: Optional[Filter] = None
@@ -934,6 +1005,13 @@ class Client:
This means only messages sent before *offset_date* will be fetched.
:return: The found messages.
.. rubric:: Example
.. code-block:: python
async for message in client.search_all_messages(query='hello'):
print(message.text)
"""
return search_all_messages(
self, limit, query=query, offset_id=offset_id, offset_date=offset_date
@@ -970,6 +1048,13 @@ class Client:
This means only messages sent before *offset_date* will be fetched.
:return: The found messages.
.. rubric:: Example
.. code-block:: python
async for message in client.search_messages(chat, query='hello'):
print(message.text)
"""
return search_messages(
self, chat, limit, query=query, offset_id=offset_id, offset_date=offset_date
@@ -988,6 +1073,9 @@ class Client:
voice: bool = False,
title: Optional[str] = None,
performer: Optional[str] = None,
caption: Optional[str] = None,
caption_markdown: Optional[str] = None,
caption_html: Optional[str] = None,
) -> Message:
"""
Send an audio file.
@@ -1002,6 +1090,12 @@ class Client:
A local file path or :class:`~telethon.types.File` to send.
The rest of parameters behave the same as they do in :meth:`send_file`.
.. rubric:: Example
.. code-block:: python
await client.send_audio(chat, 'file.ogg', voice=True)
"""
return await send_audio(
self,
@@ -1015,6 +1109,9 @@ class Client:
voice=voice,
title=title,
performer=performer,
caption=caption,
caption_markdown=caption_markdown,
caption_html=caption_html,
)
async def send_file(
@@ -1072,7 +1169,16 @@ class Client:
if *path* isn't a :class:`~telethon.types.File`.
See the documentation of :meth:`~telethon.types.File.new` to learn what they do.
See the section on :doc:`/concepts/messages` to learn about message formatting.
Note that only one *caption* parameter can be provided.
.. rubric:: Example
.. code-block:: python
login_token = await client.request_login_code('+1 23 456...')
print(login_token.timeout, 'seconds before code expires')
"""
return await send_file(
self,
@@ -1120,20 +1226,23 @@ class Client:
Message text, with no formatting.
:param text_markdown:
Message text, parsed as markdown.
Message text, parsed as CommonMark.
:param text_html:
Message text, parsed as HTML.
Note that exactly one *text* parameter must be provided.
See the section on :doc:`/concepts/messages` to learn about message formatting.
.. rubric:: Example
.. code-block:: python
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
)
async def send_photo(
@@ -1148,6 +1257,9 @@ class Client:
compress: bool = True,
width: Optional[int] = None,
height: Optional[int] = None,
caption: Optional[str] = None,
caption_markdown: Optional[str] = None,
caption_html: Optional[str] = None,
) -> Message:
"""
Send a photo file.
@@ -1166,6 +1278,12 @@ class Client:
A local file path or :class:`~telethon.types.File` to send.
The rest of parameters behave the same as they do in :meth:`send_file`.
.. rubric:: Example
.. code-block:: python
await client.send_photo(chat, 'photo.jpg', caption='Check this out!')
"""
return await send_photo(
self,
@@ -1178,6 +1296,9 @@ class Client:
compress=compress,
width=width,
height=height,
caption=caption,
caption_markdown=caption_markdown,
caption_html=caption_html,
)
async def send_video(
@@ -1194,6 +1315,9 @@ class Client:
height: Optional[int] = None,
round: bool = False,
supports_streaming: bool = False,
caption: Optional[str] = None,
caption_markdown: Optional[str] = None,
caption_html: Optional[str] = None,
) -> Message:
"""
Send a video file.
@@ -1208,6 +1332,12 @@ class Client:
A local file path or :class:`~telethon.types.File` to send.
The rest of parameters behave the same as they do in :meth:`send_file`.
.. rubric:: Example
.. code-block:: python
await client.send_video(chat, 'video.mp4', caption_markdown='*I cannot believe this just happened*')
"""
return await send_video(
self,
@@ -1222,8 +1352,20 @@ class Client:
height=height,
round=round,
supports_streaming=supports_streaming,
caption=caption,
caption_markdown=caption_markdown,
caption_html=caption_html,
)
def set_admin_rights(self, chat: ChatLike, user: ChatLike) -> None:
set_admin_rights(self, chat, user)
def set_banned_rights(self, chat: ChatLike, user: ChatLike) -> None:
set_banned_rights(self, chat, user)
def set_default_rights(self, chat: ChatLike, user: ChatLike) -> None:
set_default_rights(self, chat, user)
def set_handler_filter(
self,
handler: Callable[[Event], Awaitable[Any]],
@@ -1314,7 +1456,7 @@ class Client:
"""
await unpin_message(self, chat, message_id)
# ---
# End partially @generated
@property
def connected(self) -> bool:

View File

@@ -2,8 +2,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from ...tl import abcs, functions, types
from ..types import AsyncList, ChatLike, Dialog, User
from ...tl import functions, types
from ..types import AsyncList, ChatLike, Dialog, Draft
from ..utils import build_chat_map
if TYPE_CHECKING:
@@ -78,3 +78,29 @@ async def delete_dialog(self: Client, chat: ChatLike) -> None:
max_date=None,
)
)
class DraftList(AsyncList[Draft]):
def __init__(self, client: Client):
super().__init__()
self._client = client
self._offset = 0
async def _fetch_next(self) -> None:
result = await self._client(functions.messages.get_all_drafts())
assert isinstance(result, types.Updates)
chat_map = build_chat_map(result.users, result.chats)
self._buffer.extend(
Draft._from_raw(u, chat_map)
for u in result.updates
if isinstance(u, types.UpdateDraftMessage)
)
self._total = len(result.updates)
self._done = True
def get_drafts(self: Client) -> AsyncList[Draft]:
return DraftList(self)

View File

@@ -1,12 +1,9 @@
from __future__ import annotations
import asyncio
import hashlib
from functools import partial
from inspect import isawaitable
from io import BufferedWriter
from pathlib import Path
from typing import TYPE_CHECKING, Any, Coroutine, Optional, Tuple, Union
from typing import TYPE_CHECKING, Optional, Union
from ...tl import abcs, functions, types
from ..types import (
@@ -43,6 +40,9 @@ async def send_photo(
compress: bool = True,
width: Optional[int] = None,
height: Optional[int] = None,
caption: Optional[str] = None,
caption_markdown: Optional[str] = None,
caption_html: Optional[str] = None,
) -> Message:
return await send_file(
self,
@@ -55,6 +55,9 @@ async def send_photo(
compress=compress,
width=width,
height=height,
caption=caption,
caption_markdown=caption_markdown,
caption_html=caption_html,
)
@@ -71,6 +74,9 @@ async def send_audio(
voice: bool = False,
title: Optional[str] = None,
performer: Optional[str] = None,
caption: Optional[str] = None,
caption_markdown: Optional[str] = None,
caption_html: Optional[str] = None,
) -> Message:
return await send_file(
self,
@@ -84,6 +90,9 @@ async def send_audio(
voice=voice,
title=title,
performer=performer,
caption=caption,
caption_markdown=caption_markdown,
caption_html=caption_html,
)
@@ -101,6 +110,9 @@ async def send_video(
height: Optional[int] = None,
round: bool = False,
supports_streaming: bool = False,
caption: Optional[str] = None,
caption_markdown: Optional[str] = None,
caption_html: Optional[str] = None,
) -> Message:
return await send_file(
self,
@@ -115,6 +127,9 @@ async def send_video(
height=height,
round=round,
supports_streaming=supports_streaming,
caption=caption,
caption_markdown=caption_markdown,
caption_html=caption_html,
)

View File

@@ -319,6 +319,7 @@ class SearchList(MessageList):
self._peer: Optional[abcs.InputPeer] = None
self._limit = limit
self._query = query
self._filter = types.InputMessagesFilterEmpty()
self._offset_id = offset_id
self._offset_date = offset_date
@@ -334,7 +335,7 @@ class SearchList(MessageList):
q=self._query,
from_id=None,
top_msg_id=None,
filter=types.InputMessagesFilterEmpty(),
filter=self._filter,
min_date=0,
max_date=self._offset_date,
offset_id=self._offset_id,

View File

@@ -7,12 +7,10 @@ from typing import (
Any,
Awaitable,
Callable,
Dict,
List,
Optional,
Type,
TypeVar,
Union,
)
from ...session import Gap

View File

@@ -1,12 +1,14 @@
from .async_list import AsyncList
from .chat import Channel, Chat, ChatLike, Group, RestrictionReason, User
from .dialog import Dialog
from .draft import Draft
from .file import File, InFileLike, InWrapper, OutFileLike, OutWrapper
from .login_token import LoginToken
from .message import Message
from .meta import NoPublicConstructor
from .participant import Participant
from .password_token import PasswordToken
from .recent_action import RecentAction
__all__ = [
"AsyncList",
@@ -17,6 +19,7 @@ __all__ = [
"RestrictionReason",
"User",
"Dialog",
"Draft",
"File",
"InFileLike",
"InWrapper",
@@ -27,4 +30,5 @@ __all__ = [
"NoPublicConstructor",
"Participant",
"PasswordToken",
"RecentAction",
]

View File

@@ -0,0 +1,29 @@
from typing import Dict, List, Optional, Self
from ...session import PackedChat, PackedType
from ...tl import abcs, types
from .chat import Chat
from .meta import NoPublicConstructor
class Draft(metaclass=NoPublicConstructor):
"""
A draft message in a chat.
"""
__slots__ = ("_raw", "_chat_map")
def __init__(
self, raw: types.UpdateDraftMessage, chat_map: Dict[int, Chat]
) -> None:
self._raw = raw
self._chat_map = chat_map
@classmethod
def _from_raw(
cls, draft: types.UpdateDraftMessage, chat_map: Dict[int, Chat]
) -> Self:
return cls._create(draft, chat_map)
async def delete(self) -> None:
pass

View File

@@ -137,7 +137,7 @@ class File(metaclass=NoPublicConstructor):
self._raw = raw
@classmethod
def _try_from_raw(cls, raw: abcs.MessageMedia) -> Optional[Self]:
def _try_from_raw_message_media(cls, raw: abcs.MessageMedia) -> Optional[Self]:
if isinstance(raw, types.MessageMediaDocument):
if isinstance(raw.document, types.Document):
return cls._create(
@@ -176,30 +176,47 @@ class File(metaclass=NoPublicConstructor):
raw=raw,
)
elif isinstance(raw, types.MessageMediaPhoto):
if isinstance(raw.photo, types.Photo):
return cls._create(
path=None,
file=None,
attributes=[],
size=max(map(photo_size_byte_count, raw.photo.sizes)),
name="",
mime="image/jpeg",
photo=True,
muted=False,
input_media=types.InputMediaPhoto(
spoiler=raw.spoiler,
id=types.InputPhoto(
id=raw.photo.id,
access_hash=raw.photo.access_hash,
file_reference=raw.photo.file_reference,
),
ttl_seconds=raw.ttl_seconds,
),
raw=raw,
if raw.photo:
return cls._try_from_raw_photo(
raw.photo, spoiler=raw.spoiler, ttl_seconds=raw.ttl_seconds
)
return None
@classmethod
def _try_from_raw_photo(
cls,
raw: abcs.Photo,
*,
spoiler: bool = False,
ttl_seconds: Optional[int] = None,
) -> Optional[Self]:
if isinstance(raw, types.Photo):
return cls._create(
path=None,
file=None,
attributes=[],
size=max(map(photo_size_byte_count, raw.sizes)),
name="",
mime="image/jpeg",
photo=True,
muted=False,
input_media=types.InputMediaPhoto(
spoiler=spoiler,
id=types.InputPhoto(
id=raw.id,
access_hash=raw.access_hash,
file_reference=raw.file_reference,
),
ttl_seconds=ttl_seconds,
),
raw=types.MessageMediaPhoto(
spoiler=spoiler, photo=raw, ttl_seconds=ttl_seconds
),
)
return None
@classmethod
def new(
cls,

View File

@@ -39,8 +39,26 @@ class Message(metaclass=NoPublicConstructor):
@property
def id(self) -> int:
"""
The message identifier.
.. seealso::
:doc:`/concepts/messages`, which contains an in-depth explanation of message counters.
"""
return self._raw.id
@property
def grouped_id(self) -> Optional[int]:
"""
If the message is grouped with others in an album, return the group identifier.
Messages with the same :attr:`grouped_id` will belong to the same album.
Note that there can be messages in-between that do not have a :attr:`grouped_id`.
"""
return getattr(self._raw, "grouped_id", None)
@property
def text(self) -> Optional[str]:
return getattr(self._raw, "message", None)
@@ -91,7 +109,7 @@ class Message(metaclass=NoPublicConstructor):
def _file(self) -> Optional[File]:
return (
File._try_from_raw(self._raw.media)
File._try_from_raw_message_media(self._raw.media)
if isinstance(self._raw, types.Message) and self._raw.media
else None
)

View File

@@ -0,0 +1,29 @@
from typing import Dict, List, Optional, Self, Union
from ...session import PackedChat, PackedType
from ...tl import abcs, types
from .chat import Chat
from .meta import NoPublicConstructor
class RecentAction(metaclass=NoPublicConstructor):
"""
A recent action in a chat, also known as an "admin log event action" or :tl:`ChannelAdminLogEvent`.
Only administrators of the chat can access these.
"""
__slots__ = ("_raw", "_chat_map")
def __init__(
self,
event: abcs.ChannelAdminLogEvent,
chat_map: Dict[int, Chat],
) -> None:
assert isinstance(event, types.ChannelAdminLogEvent)
self._raw = event
self._chat_map = chat_map
@property
def id(self) -> int:
return self._raw.id

View File

@@ -1,11 +1,11 @@
import struct
from enum import Enum
from enum import IntFlag
from typing import Optional, Self
from ...tl import abcs, types
class PackedType(Enum):
class PackedType(IntFlag):
"""
The type of a :class:`PackedChat`.
"""
@@ -52,6 +52,27 @@ class PackedChat:
ty = PackedType(ty_byte & 0b0011_1111)
return cls(ty, id, access_hash if has_hash else None)
@property
def hex(self) -> str:
"""
Convenience property to convert to bytes and represent them as hexadecimal numbers:
.. code-block::
assert packed.hex == bytes(packed).hex()
"""
return bytes(self).hex()
def from_hex(cls, hex: str) -> Self:
"""
Convenience method to convert hexadecimal numbers into bytes then passed to :meth:`from_bytes`:
.. code-block::
assert PackedChat.from_hex(packed.hex) == packed
"""
return cls.from_bytes(bytes.fromhex(hex))
def is_user(self) -> bool:
return self.ty in (PackedType.USER, PackedType.BOT)
@@ -93,13 +114,13 @@ class PackedChat:
if self.is_user():
return types.InputUser(user_id=self.id, access_hash=self.access_hash or 0)
else:
raise ValueError("chat is not user")
raise TypeError("chat is not a user")
def _to_chat_id(self) -> int:
if self.is_chat():
return self.id
else:
raise ValueError("chat is not small group")
raise TypeError("chat is not a group")
def _to_input_channel(self) -> types.InputChannel:
if self.is_channel():
@@ -107,7 +128,7 @@ class PackedChat:
channel_id=self.id, access_hash=self.access_hash or 0
)
else:
raise ValueError("chat is not channel")
raise TypeError("chat is not a channel")
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):