mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-07-18 12:02:19 +03:00
Add missing implementations
This commit is contained in:
parent
5f4470dd57
commit
6a880f1ff1
|
@ -1,6 +1,6 @@
|
||||||
from ._impl import tl as _tl
|
from ._impl import tl as _tl
|
||||||
from ._impl.client import Client, Config
|
from ._impl.client import Client, Config
|
||||||
from ._impl.errors import errors
|
from ._impl.client.errors import errors
|
||||||
from ._impl.mtproto import RpcError
|
from ._impl.mtproto import RpcError
|
||||||
from ._impl.session import Session
|
from ._impl.session import Session
|
||||||
from .version import __version__
|
from .version import __version__
|
||||||
|
|
|
@ -4,8 +4,8 @@ import getpass
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
|
from ...crypto import two_factor_auth
|
||||||
from ...mtproto import RpcError
|
from ...mtproto import RpcError
|
||||||
from ...session import Session
|
|
||||||
from ...session import User as SessionUser
|
from ...session import User as SessionUser
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
from ..types import LoginToken, PasswordToken, User
|
from ..types import LoginToken, PasswordToken, User
|
||||||
|
@ -192,8 +192,46 @@ async def get_password_information(client: Client) -> PasswordToken:
|
||||||
async def check_password(
|
async def check_password(
|
||||||
self: Client, token: PasswordToken, password: Union[str, bytes]
|
self: Client, token: PasswordToken, password: Union[str, bytes]
|
||||||
) -> User:
|
) -> User:
|
||||||
self, token, password
|
algo = token._password.current_algo
|
||||||
raise NotImplementedError
|
if not isinstance(
|
||||||
|
algo, types.PasswordKdfAlgoSha256Sha256Pbkdf2HmacshA512Iter100000Sha256ModPow
|
||||||
|
):
|
||||||
|
raise RuntimeError("unrecognised 2FA algorithm")
|
||||||
|
|
||||||
|
if not two_factor_auth.check_p_and_g(algo.p, algo.g):
|
||||||
|
token = await get_password_information(self)
|
||||||
|
if not isinstance(
|
||||||
|
algo,
|
||||||
|
types.PasswordKdfAlgoSha256Sha256Pbkdf2HmacshA512Iter100000Sha256ModPow,
|
||||||
|
):
|
||||||
|
raise RuntimeError("unrecognised 2FA algorithm")
|
||||||
|
if not two_factor_auth.check_p_and_g(algo.p, algo.g):
|
||||||
|
raise RuntimeError("failed to get correct password information")
|
||||||
|
|
||||||
|
assert token._password.srp_id is not None
|
||||||
|
assert token._password.srp_B is not None
|
||||||
|
|
||||||
|
two_fa = two_factor_auth.calculate_2fa(
|
||||||
|
salt1=algo.salt1,
|
||||||
|
salt2=algo.salt2,
|
||||||
|
g=algo.g,
|
||||||
|
p=algo.p,
|
||||||
|
g_b=token._password.srp_B,
|
||||||
|
a=token._password.secure_random,
|
||||||
|
password=password.encode("utf-8") if isinstance(password, str) else password,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self(
|
||||||
|
functions.auth.check_password(
|
||||||
|
password=types.InputCheckPasswordSrp(
|
||||||
|
srp_id=token._password.srp_id,
|
||||||
|
A=two_fa.g_a,
|
||||||
|
M1=two_fa.m1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await complete_login(self, result)
|
||||||
|
|
||||||
|
|
||||||
async def sign_out(self: Client) -> None:
|
async def sign_out(self: Client) -> None:
|
||||||
|
|
|
@ -1,11 +1,83 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from ...tl import abcs, functions, types
|
||||||
|
from ..types import AsyncList, ChatLike, Participant
|
||||||
|
from ..utils import build_chat_map
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
|
||||||
|
|
||||||
def get_participants(self: Client) -> None:
|
class ParticipantList(AsyncList[Participant]):
|
||||||
self
|
def __init__(
|
||||||
raise NotImplementedError
|
self,
|
||||||
|
client: Client,
|
||||||
|
chat: ChatLike,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self._client = client
|
||||||
|
self._chat = chat
|
||||||
|
self._peer: Optional[abcs.InputPeer] = 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_peer()
|
||||||
|
|
||||||
|
if isinstance(self._peer, types.InputPeerChannel):
|
||||||
|
result = await self._client(
|
||||||
|
functions.channels.get_participants(
|
||||||
|
channel=types.InputChannel(
|
||||||
|
channel_id=self._peer.channel_id,
|
||||||
|
access_hash=self._peer.access_hash,
|
||||||
|
),
|
||||||
|
filter=types.ChannelParticipantsRecent(),
|
||||||
|
offset=self._offset,
|
||||||
|
limit=200,
|
||||||
|
hash=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert isinstance(result, types.channels.ChannelParticipants)
|
||||||
|
|
||||||
|
chat_map = build_chat_map(result.users, result.chats)
|
||||||
|
|
||||||
|
self._buffer.extend(
|
||||||
|
Participant._from_raw_channel(p, chat_map) for p in result.participants
|
||||||
|
)
|
||||||
|
self._total = result.count
|
||||||
|
|
||||||
|
elif isinstance(self._peer, types.InputPeerChat):
|
||||||
|
result = await self._client(
|
||||||
|
functions.messages.get_full_chat(chat_id=self._peer.chat_id) # type: ignore [arg-type]
|
||||||
|
)
|
||||||
|
assert isinstance(result, types.messages.ChatFull)
|
||||||
|
assert isinstance(result.full_chat, types.ChatFull)
|
||||||
|
|
||||||
|
chat_map = build_chat_map(result.users, result.chats)
|
||||||
|
|
||||||
|
participants = result.full_chat.participants
|
||||||
|
if isinstance(participants, types.ChatParticipantsForbidden):
|
||||||
|
if participants.self_participant:
|
||||||
|
self._buffer.append(
|
||||||
|
Participant._from_raw_chat(
|
||||||
|
participants.self_participant, chat_map
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif isinstance(participants, types.ChatParticipants):
|
||||||
|
self._buffer.extend(
|
||||||
|
Participant._from_raw_chat(p, chat_map)
|
||||||
|
for p in participants.participants
|
||||||
|
)
|
||||||
|
|
||||||
|
self._total = len(self._buffer)
|
||||||
|
self._done = True
|
||||||
|
else:
|
||||||
|
raise TypeError("can only get participants from channels and groups")
|
||||||
|
|
||||||
|
|
||||||
|
def get_participants(self: Client, chat: ChatLike) -> AsyncList[Participant]:
|
||||||
|
return ParticipantList(self, chat)
|
||||||
|
|
|
@ -35,12 +35,13 @@ from ..types import (
|
||||||
AsyncList,
|
AsyncList,
|
||||||
Chat,
|
Chat,
|
||||||
ChatLike,
|
ChatLike,
|
||||||
|
Dialog,
|
||||||
File,
|
File,
|
||||||
InFileLike,
|
InFileLike,
|
||||||
LoginToken,
|
LoginToken,
|
||||||
MediaLike,
|
|
||||||
Message,
|
Message,
|
||||||
OutFileLike,
|
OutFileLike,
|
||||||
|
Participant,
|
||||||
PasswordToken,
|
PasswordToken,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
@ -58,7 +59,7 @@ from .chats import get_participants
|
||||||
from .dialogs import delete_dialog, get_dialogs
|
from .dialogs import delete_dialog, get_dialogs
|
||||||
from .files import (
|
from .files import (
|
||||||
download,
|
download,
|
||||||
iter_download,
|
get_file_bytes,
|
||||||
send_audio,
|
send_audio,
|
||||||
send_file,
|
send_file,
|
||||||
send_photo,
|
send_photo,
|
||||||
|
@ -184,7 +185,7 @@ class Client:
|
||||||
self._chat_hashes = ChatHashCache(None)
|
self._chat_hashes = ChatHashCache(None)
|
||||||
self._last_update_limit_warn: Optional[float] = None
|
self._last_update_limit_warn: Optional[float] = None
|
||||||
self._updates: asyncio.Queue[
|
self._updates: asyncio.Queue[
|
||||||
Tuple[abcs.Update, Dict[int, Union[abcs.User, abcs.Chat]]]
|
Tuple[abcs.Update, Dict[int, Chat]]
|
||||||
] = asyncio.Queue(maxsize=self._config.update_queue_limit or 0)
|
] = asyncio.Queue(maxsize=self._config.update_queue_limit or 0)
|
||||||
self._dispatcher: Optional[asyncio.Task[None]] = None
|
self._dispatcher: Optional[asyncio.Task[None]] = None
|
||||||
self._downloader_map = object()
|
self._downloader_map = object()
|
||||||
|
@ -315,8 +316,29 @@ class Client:
|
||||||
"""
|
"""
|
||||||
await connect(self)
|
await connect(self)
|
||||||
|
|
||||||
async def delete_dialog(self) -> None:
|
async def delete_dialog(self, chat: ChatLike) -> None:
|
||||||
await delete_dialog(self)
|
"""
|
||||||
|
Delete a dialog.
|
||||||
|
|
||||||
|
This lets you leave a group, unsubscribe from a channel, or delete a one-to-one private conversation.
|
||||||
|
|
||||||
|
Note that the group or channel will not be deleted.
|
||||||
|
|
||||||
|
Note that bot accounts do not have dialogs, so this method will fail.
|
||||||
|
|
||||||
|
:param chat:
|
||||||
|
The :term:`chat` representing the dialog to delete.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async for dialog in client.iter_dialogs():
|
||||||
|
if 'dog pictures' in dialog.chat.full_name:
|
||||||
|
# You've realized you're more of a cat person
|
||||||
|
await client.delete_dialog(dialog.chat)
|
||||||
|
"""
|
||||||
|
await delete_dialog(self, chat)
|
||||||
|
|
||||||
async def delete_messages(
|
async def delete_messages(
|
||||||
self, chat: ChatLike, message_ids: List[int], *, revoke: bool = True
|
self, chat: ChatLike, message_ids: List[int], *, revoke: bool = True
|
||||||
|
@ -374,12 +396,12 @@ class Client:
|
||||||
"""
|
"""
|
||||||
await disconnect(self)
|
await disconnect(self)
|
||||||
|
|
||||||
async def download(self, media: MediaLike, file: OutFileLike) -> None:
|
async def download(self, media: File, file: Union[str, Path, OutFileLike]) -> None:
|
||||||
"""
|
"""
|
||||||
Download a file.
|
Download a file.
|
||||||
|
|
||||||
:param media:
|
:param media:
|
||||||
The media to download.
|
The media file to download.
|
||||||
This will often come from :attr:`telethon.types.Message.file`.
|
This will often come from :attr:`telethon.types.Message.file`.
|
||||||
|
|
||||||
:param file:
|
:param file:
|
||||||
|
@ -387,6 +409,10 @@ class Client:
|
||||||
Note that the extension is not automatically added to the path.
|
Note that the extension is not automatically added to the path.
|
||||||
You can get the file extension with :attr:`telethon.types.File.ext`.
|
You can get the file extension with :attr:`telethon.types.File.ext`.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
If the file already exists, it will be overwritten!
|
||||||
|
|
||||||
.. rubric:: Example
|
.. rubric:: Example
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -400,7 +426,7 @@ class Client:
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
:meth:`iter_download`, for fine-grained control over the download.
|
:meth:`get_file_bytes`, for more control over the download.
|
||||||
"""
|
"""
|
||||||
await download(self, media, file)
|
await download(self, media, file)
|
||||||
|
|
||||||
|
@ -485,7 +511,7 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return await forward_messages(self, target, message_ids, source)
|
return await forward_messages(self, target, message_ids, source)
|
||||||
|
|
||||||
async def get_contacts(self) -> AsyncList[User]:
|
def get_contacts(self) -> AsyncList[User]:
|
||||||
"""
|
"""
|
||||||
Get the users in your contact list.
|
Get the users in your contact list.
|
||||||
|
|
||||||
|
@ -498,10 +524,29 @@ class Client:
|
||||||
async for user in client.get_contacts():
|
async for user in client.get_contacts():
|
||||||
print(user.full_name, user.id)
|
print(user.full_name, user.id)
|
||||||
"""
|
"""
|
||||||
return await get_contacts(self)
|
return get_contacts(self)
|
||||||
|
|
||||||
def get_dialogs(self) -> None:
|
def get_dialogs(self) -> AsyncList[Dialog]:
|
||||||
get_dialogs(self)
|
"""
|
||||||
|
Get the dialogs you're part of.
|
||||||
|
|
||||||
|
This list of includes the groups you've joined, channels you've subscribed to, and open one-to-one private conversations.
|
||||||
|
|
||||||
|
Note that bot accounts do not have dialogs, so this method will fail.
|
||||||
|
|
||||||
|
:return: Your dialogs.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async for dialog in client.get_dialogs():
|
||||||
|
print(
|
||||||
|
dialog.chat.full_name,
|
||||||
|
dialog.last_message.text if dialog.last_message else ''
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
return get_dialogs(self)
|
||||||
|
|
||||||
def get_handler_filter(
|
def get_handler_filter(
|
||||||
self, handler: Callable[[Event], Awaitable[Any]]
|
self, handler: Callable[[Event], Awaitable[Any]]
|
||||||
|
@ -564,8 +609,6 @@ class Client:
|
||||||
"""
|
"""
|
||||||
Get the message history from a :term:`chat`.
|
Get the message history from a :term:`chat`.
|
||||||
|
|
||||||
Edit a message.
|
|
||||||
|
|
||||||
:param chat:
|
:param chat:
|
||||||
The :term:`chat` where the message to edit is.
|
The :term:`chat` where the message to edit is.
|
||||||
|
|
||||||
|
@ -604,8 +647,24 @@ class Client:
|
||||||
) -> AsyncList[Message]:
|
) -> AsyncList[Message]:
|
||||||
return get_messages_with_ids(self, chat, message_ids)
|
return get_messages_with_ids(self, chat, message_ids)
|
||||||
|
|
||||||
def get_participants(self) -> None:
|
def get_participants(self, chat: ChatLike) -> AsyncList[Participant]:
|
||||||
get_participants(self)
|
"""
|
||||||
|
Get the participants in a group or channel, along with their permissions.
|
||||||
|
|
||||||
|
Note that Telegram is rather strict when it comes to fetching members.
|
||||||
|
It is very likely that you will not be able to fetch all the members.
|
||||||
|
There is no way to bypass this.
|
||||||
|
|
||||||
|
:return: The participants.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async for participant in client.get_participants(chat):
|
||||||
|
print(participant.user.full_name)
|
||||||
|
"""
|
||||||
|
return get_participants(self, chat)
|
||||||
|
|
||||||
async def inline_query(
|
async def inline_query(
|
||||||
self, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
|
self, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
|
||||||
|
@ -674,8 +733,31 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return await is_authorized(self)
|
return await is_authorized(self)
|
||||||
|
|
||||||
async def iter_download(self) -> None:
|
def get_file_bytes(self, media: File) -> AsyncList[bytes]:
|
||||||
await iter_download(self)
|
"""
|
||||||
|
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 on(
|
def on(
|
||||||
self, event_cls: Type[Event], filter: Optional[Filter] = None
|
self, event_cls: Type[Event], filter: Optional[Filter] = None
|
||||||
|
@ -815,8 +897,8 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return await resolve_to_packed(self, chat)
|
return await resolve_to_packed(self, chat)
|
||||||
|
|
||||||
async def resolve_username(self) -> Chat:
|
async def resolve_username(self, username: str) -> Chat:
|
||||||
return await resolve_username(self)
|
return await resolve_username(self, username)
|
||||||
|
|
||||||
async def run_until_disconnected(self) -> None:
|
async def run_until_disconnected(self) -> None:
|
||||||
await run_until_disconnected(self)
|
await run_until_disconnected(self)
|
||||||
|
|
|
@ -2,15 +2,79 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ...tl import abcs, functions, types
|
||||||
|
from ..types import AsyncList, ChatLike, Dialog, User
|
||||||
|
from ..utils import build_chat_map
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
|
||||||
|
|
||||||
def get_dialogs(self: Client) -> None:
|
class DialogList(AsyncList[Dialog]):
|
||||||
self
|
def __init__(self, client: Client):
|
||||||
raise NotImplementedError
|
super().__init__()
|
||||||
|
self._client = client
|
||||||
|
self._offset = 0
|
||||||
|
|
||||||
|
async def _fetch_next(self) -> None:
|
||||||
|
result = await self._client(
|
||||||
|
functions.messages.get_dialogs(
|
||||||
|
exclude_pinned=False,
|
||||||
|
folder_id=None,
|
||||||
|
offset_date=0,
|
||||||
|
offset_id=0,
|
||||||
|
offset_peer=types.InputPeerEmpty(),
|
||||||
|
limit=0,
|
||||||
|
hash=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(result, types.messages.Dialogs):
|
||||||
|
self._total = len(result.dialogs)
|
||||||
|
self._done = True
|
||||||
|
elif isinstance(result, types.messages.DialogsSlice):
|
||||||
|
self._total = result.count
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
assert isinstance(result, (types.messages.Dialogs, types.messages.DialogsSlice))
|
||||||
|
|
||||||
|
chat_map = build_chat_map(result.users, result.chats)
|
||||||
|
|
||||||
|
self._buffer.extend(Dialog._from_raw(d, chat_map) for d in result.dialogs)
|
||||||
|
|
||||||
|
|
||||||
async def delete_dialog(self: Client) -> None:
|
def get_dialogs(self: Client) -> AsyncList[Dialog]:
|
||||||
self
|
return DialogList(self)
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
async def delete_dialog(self: Client, chat: ChatLike) -> None:
|
||||||
|
peer = (await self._resolve_to_packed(chat))._to_input_peer()
|
||||||
|
if isinstance(peer, types.InputPeerChannel):
|
||||||
|
await self(
|
||||||
|
functions.channels.leave_channel(
|
||||||
|
channel=types.InputChannel(
|
||||||
|
channel_id=peer.channel_id,
|
||||||
|
access_hash=peer.access_hash,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif isinstance(peer, types.InputPeerChat):
|
||||||
|
await self(
|
||||||
|
functions.messages.delete_chat_user(
|
||||||
|
revoke_history=False,
|
||||||
|
chat_id=peer.chat_id,
|
||||||
|
user_id=types.InputUserSelf(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif isinstance(peer, types.InputPeerUser):
|
||||||
|
await self(
|
||||||
|
functions.messages.delete_history(
|
||||||
|
just_clear=False,
|
||||||
|
revoke=False,
|
||||||
|
peer=peer,
|
||||||
|
max_id=0,
|
||||||
|
min_date=None,
|
||||||
|
max_date=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -1,11 +1,23 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from functools import partial
|
||||||
|
from inspect import isawaitable
|
||||||
|
from io import BufferedWriter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, Any, Coroutine, Optional, Tuple, Union
|
||||||
|
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
from ..types import ChatLike, File, InFileLike, MediaLike, Message, OutFileLike
|
from ..types import (
|
||||||
|
AsyncList,
|
||||||
|
ChatLike,
|
||||||
|
File,
|
||||||
|
InFileLike,
|
||||||
|
Message,
|
||||||
|
OutFileLike,
|
||||||
|
OutWrapper,
|
||||||
|
)
|
||||||
from ..utils import generate_random_id
|
from ..utils import generate_random_id
|
||||||
from .messages import parse_message
|
from .messages import parse_message
|
||||||
|
|
||||||
|
@ -222,73 +234,102 @@ async def upload(
|
||||||
hash_md5 = hashlib.md5()
|
hash_md5 = hashlib.md5()
|
||||||
is_big = file._size > BIG_FILE_SIZE
|
is_big = file._size > BIG_FILE_SIZE
|
||||||
|
|
||||||
while uploaded != file._size:
|
fd = file._open()
|
||||||
chunk = await file._read(MAX_CHUNK_SIZE - len(buffer))
|
try:
|
||||||
if not chunk:
|
while uploaded != file._size:
|
||||||
raise ValueError("unexpected end-of-file")
|
chunk = await fd.read(MAX_CHUNK_SIZE - len(buffer))
|
||||||
|
if not chunk:
|
||||||
|
raise ValueError("unexpected end-of-file")
|
||||||
|
|
||||||
if len(chunk) == MAX_CHUNK_SIZE or uploaded + len(chunk) == file._size:
|
if len(chunk) == MAX_CHUNK_SIZE or uploaded + len(chunk) == file._size:
|
||||||
to_store = chunk
|
to_store = chunk
|
||||||
else:
|
|
||||||
buffer += chunk
|
|
||||||
if len(buffer) == MAX_CHUNK_SIZE:
|
|
||||||
to_store = buffer
|
|
||||||
else:
|
else:
|
||||||
continue
|
buffer += chunk
|
||||||
|
if len(buffer) == MAX_CHUNK_SIZE:
|
||||||
|
to_store = buffer
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
if is_big:
|
if is_big:
|
||||||
await client(
|
await client(
|
||||||
functions.upload.save_big_file_part(
|
functions.upload.save_big_file_part(
|
||||||
file_id=file_id,
|
file_id=file_id,
|
||||||
file_part=part,
|
file_part=part,
|
||||||
file_total_parts=part,
|
file_total_parts=part,
|
||||||
bytes=to_store,
|
bytes=to_store,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
else:
|
||||||
else:
|
await client(
|
||||||
await client(
|
functions.upload.save_file_part(
|
||||||
functions.upload.save_file_part(
|
file_id=file_id, file_part=total_parts, bytes=to_store
|
||||||
file_id=file_id, file_part=total_parts, bytes=to_store
|
)
|
||||||
)
|
)
|
||||||
)
|
hash_md5.update(to_store)
|
||||||
hash_md5.update(to_store)
|
|
||||||
|
|
||||||
buffer.clear()
|
buffer.clear()
|
||||||
part += 1
|
part += 1
|
||||||
|
finally:
|
||||||
|
fd.close()
|
||||||
|
|
||||||
if file._size > BIG_FILE_SIZE:
|
if file._size > BIG_FILE_SIZE:
|
||||||
types.InputFileBig(
|
return types.InputFileBig(
|
||||||
id=file_id,
|
id=file_id,
|
||||||
parts=total_parts,
|
parts=total_parts,
|
||||||
name=file._name,
|
name=file._name,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
types.InputFile(
|
return types.InputFile(
|
||||||
id=file_id,
|
id=file_id,
|
||||||
parts=total_parts,
|
parts=total_parts,
|
||||||
name=file._name,
|
name=file._name,
|
||||||
md5_checksum=hash_md5.hexdigest(),
|
md5_checksum=hash_md5.hexdigest(),
|
||||||
)
|
)
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
async def iter_download(self: Client) -> None:
|
class FileBytesList(AsyncList[bytes]):
|
||||||
raise NotImplementedError
|
def __init__(
|
||||||
# result = self(
|
self,
|
||||||
# functions.upload.get_file(
|
client: Client,
|
||||||
# precise=False,
|
file: File,
|
||||||
# cdn_supported=False,
|
):
|
||||||
# location=types.InputFileLocation(),
|
super().__init__()
|
||||||
# offset=0,
|
self._client = client
|
||||||
# limit=MAX_CHUNK_SIZE,
|
self._loc = file._input_location()
|
||||||
# )
|
self._offset = 0
|
||||||
# )
|
|
||||||
# assert isinstance(result, types.upload.File)
|
async def _fetch_next(self) -> None:
|
||||||
# if len(result.bytes) < MAX_CHUNK_SIZE:
|
result = await self._client(
|
||||||
# done
|
functions.upload.get_file(
|
||||||
# else:
|
precise=False,
|
||||||
# offset += MAX_CHUNK_SIZE
|
cdn_supported=False,
|
||||||
|
location=self._loc,
|
||||||
|
offset=self._offset,
|
||||||
|
limit=MAX_CHUNK_SIZE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert isinstance(result, types.upload.File)
|
||||||
|
|
||||||
|
if result.bytes:
|
||||||
|
self._offset += MAX_CHUNK_SIZE
|
||||||
|
self._buffer.append(result.bytes)
|
||||||
|
|
||||||
|
self._done = len(result.bytes) < MAX_CHUNK_SIZE
|
||||||
|
|
||||||
|
|
||||||
async def download(self: Client, media: MediaLike, file: OutFileLike) -> None:
|
def get_file_bytes(self: Client, media: File) -> AsyncList[bytes]:
|
||||||
raise NotImplementedError
|
return FileBytesList(self, media)
|
||||||
|
|
||||||
|
|
||||||
|
async def download(
|
||||||
|
self: Client, media: File, file: Union[str, Path, OutFileLike]
|
||||||
|
) -> None:
|
||||||
|
fd = OutWrapper(file)
|
||||||
|
try:
|
||||||
|
async for chunk in get_file_bytes(self, media):
|
||||||
|
ret = fd.write(chunk)
|
||||||
|
if isawaitable(ret):
|
||||||
|
await ret
|
||||||
|
|
||||||
|
finally:
|
||||||
|
fd.close()
|
||||||
|
|
|
@ -7,8 +7,8 @@ from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union
|
||||||
from ...session import PackedChat
|
from ...session import PackedChat
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
from ..parsers import parse_html_message, parse_markdown_message
|
from ..parsers import parse_html_message, parse_markdown_message
|
||||||
from ..types import AsyncList, ChatLike, Message
|
from ..types import AsyncList, Chat, ChatLike, Message
|
||||||
from ..utils import generate_random_id
|
from ..utils import build_chat_map, generate_random_id, peer_id
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
@ -147,30 +147,47 @@ async def forward_messages(
|
||||||
|
|
||||||
|
|
||||||
class MessageList(AsyncList[Message]):
|
class MessageList(AsyncList[Message]):
|
||||||
def _extend_buffer(self, client: Client, messages: abcs.messages.Messages) -> None:
|
def _extend_buffer(
|
||||||
|
self, client: Client, messages: abcs.messages.Messages
|
||||||
|
) -> Dict[int, Chat]:
|
||||||
if isinstance(messages, types.messages.Messages):
|
if isinstance(messages, types.messages.Messages):
|
||||||
self._buffer.extend(Message._from_raw(m) for m in messages.messages)
|
chat_map = build_chat_map(messages.users, messages.chats)
|
||||||
|
self._buffer.extend(
|
||||||
|
Message._from_raw(client, m, chat_map) for m in messages.messages
|
||||||
|
)
|
||||||
self._total = len(messages.messages)
|
self._total = len(messages.messages)
|
||||||
self._done = True
|
self._done = True
|
||||||
|
return chat_map
|
||||||
elif isinstance(messages, types.messages.MessagesSlice):
|
elif isinstance(messages, types.messages.MessagesSlice):
|
||||||
self._buffer.extend(Message._from_raw(m) for m in messages.messages)
|
chat_map = build_chat_map(messages.users, messages.chats)
|
||||||
|
self._buffer.extend(
|
||||||
|
Message._from_raw(client, m, chat_map) for m in messages.messages
|
||||||
|
)
|
||||||
self._total = messages.count
|
self._total = messages.count
|
||||||
|
return chat_map
|
||||||
elif isinstance(messages, types.messages.ChannelMessages):
|
elif isinstance(messages, types.messages.ChannelMessages):
|
||||||
self._buffer.extend(Message._from_raw(m) for m in messages.messages)
|
chat_map = build_chat_map(messages.users, messages.chats)
|
||||||
|
self._buffer.extend(
|
||||||
|
Message._from_raw(client, m, chat_map) for m in messages.messages
|
||||||
|
)
|
||||||
self._total = messages.count
|
self._total = messages.count
|
||||||
|
return chat_map
|
||||||
elif isinstance(messages, types.messages.MessagesNotModified):
|
elif isinstance(messages, types.messages.MessagesNotModified):
|
||||||
self._total = messages.count
|
self._total = messages.count
|
||||||
|
return {}
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("unexpected case")
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
def _last_non_empty_message(self) -> Message:
|
def _last_non_empty_message(
|
||||||
|
self,
|
||||||
|
) -> Union[types.Message, types.MessageService, types.MessageEmpty]:
|
||||||
return next(
|
return next(
|
||||||
(
|
(
|
||||||
m
|
m._raw
|
||||||
for m in reversed(self._buffer)
|
for m in reversed(self._buffer)
|
||||||
if not isinstance(m._raw, types.MessageEmpty)
|
if not isinstance(m._raw, types.MessageEmpty)
|
||||||
),
|
),
|
||||||
Message._from_raw(types.MessageEmpty(id=0, peer_id=None)),
|
types.MessageEmpty(id=0, peer_id=None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -192,6 +209,8 @@ class HistoryList(MessageList):
|
||||||
self._offset_id = offset_id
|
self._offset_id = offset_id
|
||||||
self._offset_date = offset_date
|
self._offset_date = offset_date
|
||||||
|
|
||||||
|
self._done = limit <= 0
|
||||||
|
|
||||||
async def _fetch_next(self) -> None:
|
async def _fetch_next(self) -> None:
|
||||||
if self._peer is None:
|
if self._peer is None:
|
||||||
self._peer = (
|
self._peer = (
|
||||||
|
@ -213,10 +232,11 @@ 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
|
||||||
if self._buffer:
|
if self._buffer:
|
||||||
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._raw, "date", None)) is not None:
|
if (date := getattr(last, "date", None)) is not None:
|
||||||
self._offset_date = date
|
self._offset_date = date
|
||||||
|
|
||||||
|
|
||||||
|
@ -331,7 +351,7 @@ class SearchList(MessageList):
|
||||||
if self._buffer:
|
if self._buffer:
|
||||||
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._raw, "date", None)) is not None:
|
if (date := getattr(last, "date", None)) is not None:
|
||||||
self._offset_date = date
|
self._offset_date = date
|
||||||
|
|
||||||
|
|
||||||
|
@ -388,20 +408,21 @@ class GlobalSearchList(MessageList):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._extend_buffer(self._client, result)
|
chat_map = self._extend_buffer(self._client, result)
|
||||||
self._limit -= len(self._buffer)
|
self._limit -= len(self._buffer)
|
||||||
if self._buffer:
|
if self._buffer:
|
||||||
last = self._last_non_empty_message()
|
last = self._last_non_empty_message()
|
||||||
last_packed = last.chat.pack()
|
|
||||||
|
|
||||||
self._offset_id = self._buffer[-1].id
|
self._offset_id = self._buffer[-1].id
|
||||||
if (date := getattr(last._raw, "date", None)) is not None:
|
if (date := getattr(last, "date", None)) is not None:
|
||||||
self._offset_date = date
|
self._offset_date = date
|
||||||
if isinstance(result, types.messages.MessagesSlice):
|
if isinstance(result, types.messages.MessagesSlice):
|
||||||
self._offset_rate = result.next_rate or 0
|
self._offset_rate = result.next_rate or 0
|
||||||
self._offset_peer = (
|
|
||||||
last_packed._to_input_peer() if last_packed else types.InputPeerEmpty()
|
self._offset_peer = types.InputPeerEmpty()
|
||||||
)
|
if last.peer_id and (chat := chat_map.get(peer_id(last.peer_id))):
|
||||||
|
if packed := chat.pack():
|
||||||
|
self._offset_peer = packed._to_input_peer()
|
||||||
|
|
||||||
|
|
||||||
def search_all_messages(
|
def search_all_messages(
|
||||||
|
@ -483,7 +504,9 @@ class MessageMap:
|
||||||
|
|
||||||
def _empty(self, id: int = 0) -> Message:
|
def _empty(self, id: int = 0) -> Message:
|
||||||
return Message._from_raw(
|
return Message._from_raw(
|
||||||
types.MessageEmpty(id=id, peer_id=self._client._input_to_peer(self._peer))
|
self._client,
|
||||||
|
types.MessageEmpty(id=id, peer_id=self._client._input_to_peer(self._peer)),
|
||||||
|
{},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -492,13 +515,12 @@ def build_message_map(
|
||||||
result: abcs.Updates,
|
result: abcs.Updates,
|
||||||
peer: Optional[abcs.InputPeer],
|
peer: Optional[abcs.InputPeer],
|
||||||
) -> MessageMap:
|
) -> MessageMap:
|
||||||
if isinstance(result, types.UpdateShort):
|
if isinstance(result, (types.Updates, types.UpdatesCombined)):
|
||||||
updates = [result.update]
|
|
||||||
entities: Dict[int, object] = {}
|
|
||||||
elif isinstance(result, (types.Updates, types.UpdatesCombined)):
|
|
||||||
updates = result.updates
|
updates = result.updates
|
||||||
entities = {}
|
chat_map = build_chat_map(result.users, result.chats)
|
||||||
raise NotImplementedError()
|
elif isinstance(result, types.UpdateShort):
|
||||||
|
updates = [result.update]
|
||||||
|
chat_map = {}
|
||||||
else:
|
else:
|
||||||
return MessageMap(client, peer, {}, {})
|
return MessageMap(client, peer, {}, {})
|
||||||
|
|
||||||
|
@ -518,14 +540,8 @@ def build_message_map(
|
||||||
types.UpdateNewScheduledMessage,
|
types.UpdateNewScheduledMessage,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert isinstance(
|
msg = Message._from_raw(client, update.message, chat_map)
|
||||||
update.message,
|
id_to_message[msg.id] = msg
|
||||||
(types.Message, types.MessageService, types.MessageEmpty),
|
|
||||||
)
|
|
||||||
id_to_message[update.message.id] = Message._from_raw(update.message)
|
|
||||||
|
|
||||||
elif isinstance(update, types.UpdateMessagePoll):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
return MessageMap(
|
return MessageMap(
|
||||||
client,
|
client,
|
||||||
|
|
|
@ -14,6 +14,7 @@ from ...mtsender import connect_with_auth
|
||||||
from ...session import DataCenter, Session
|
from ...session import DataCenter, Session
|
||||||
from ...session import User as SessionUser
|
from ...session import User as SessionUser
|
||||||
from ...tl import LAYER, Request, functions
|
from ...tl import LAYER, Request, functions
|
||||||
|
from ..errors import adapt_rpc
|
||||||
from .updates import dispatcher, process_socket_updates
|
from .updates import dispatcher, process_socket_updates
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -217,7 +218,7 @@ async def invoke_request(
|
||||||
rx = sender.enqueue(request)
|
rx = sender.enqueue(request)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
raise
|
raise adapt_rpc(e) from None
|
||||||
return request.deserialize_response(response)
|
return request.deserialize_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ from ...session import Gap
|
||||||
from ...tl import abcs
|
from ...tl import abcs
|
||||||
from ..events import Event as EventBase
|
from ..events import Event as EventBase
|
||||||
from ..events.filters import Filter
|
from ..events.filters import Filter
|
||||||
|
from ..utils import build_chat_map
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
@ -107,14 +108,11 @@ def extend_update_queue(
|
||||||
users: List[abcs.User],
|
users: List[abcs.User],
|
||||||
chats: List[abcs.Chat],
|
chats: List[abcs.Chat],
|
||||||
) -> None:
|
) -> None:
|
||||||
entities: Dict[int, Union[abcs.User, abcs.Chat]] = {
|
chat_map = build_chat_map(users, chats)
|
||||||
getattr(u, "id", None) or 0: u for u in users
|
|
||||||
}
|
|
||||||
entities.update({getattr(c, "id", None) or 0: c for c in chats})
|
|
||||||
|
|
||||||
for update in updates:
|
for update in updates:
|
||||||
try:
|
try:
|
||||||
client._updates.put_nowait((update, entities))
|
client._updates.put_nowait((update, chat_map))
|
||||||
except asyncio.QueueFull:
|
except asyncio.QueueFull:
|
||||||
now = asyncio.get_running_loop().time()
|
now = asyncio.get_running_loop().time()
|
||||||
if client._last_update_limit_warn is None or (
|
if client._last_update_limit_warn is None or (
|
||||||
|
@ -128,9 +126,9 @@ def extend_update_queue(
|
||||||
|
|
||||||
async def dispatcher(client: Client) -> None:
|
async def dispatcher(client: Client) -> None:
|
||||||
while client.connected:
|
while client.connected:
|
||||||
update, entities = await client._updates.get()
|
update, chat_map = await client._updates.get()
|
||||||
for event_cls, handlers in client._handlers.items():
|
for event_cls, handlers in client._handlers.items():
|
||||||
if event := event_cls._try_from_update(client, update):
|
if event := event_cls._try_from_update(client, update, chat_map):
|
||||||
for handler, filter in handlers:
|
for handler, filter in handlers:
|
||||||
if not filter or filter(event):
|
if not filter or filter(event):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -2,27 +2,67 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from ...mtproto import RpcError
|
||||||
from ...session import PackedChat, PackedType
|
from ...session import PackedChat, PackedType
|
||||||
from ...tl import abcs, types
|
from ...tl import abcs, functions, types
|
||||||
from ..types import AsyncList, Channel, Chat, ChatLike, Group, User
|
from ..types import AsyncList, Channel, Chat, ChatLike, Group, User
|
||||||
|
from ..utils import build_chat_map, peer_id
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
|
||||||
|
|
||||||
async def get_me(self: Client) -> Optional[User]:
|
async def get_me(self: Client) -> Optional[User]:
|
||||||
self
|
try:
|
||||||
raise NotImplementedError
|
result = await self(functions.users.get_users(id=[types.InputUserSelf()]))
|
||||||
|
except RpcError as e:
|
||||||
|
if e.code == 401:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
return User._from_raw(result[0])
|
||||||
|
|
||||||
|
|
||||||
async def get_contacts(self: Client) -> AsyncList[User]:
|
class ContactList(AsyncList[User]):
|
||||||
self
|
def __init__(self, client: Client):
|
||||||
raise NotImplementedError
|
super().__init__()
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
async def _fetch_next(self) -> None:
|
||||||
|
result = await self._client(functions.contacts.get_contacts(hash=0))
|
||||||
|
assert isinstance(result, types.contacts.Contacts)
|
||||||
|
|
||||||
|
self._buffer.extend(User._from_raw(u) for u in result.users)
|
||||||
|
self._total = len(self._buffer)
|
||||||
|
self._done = True
|
||||||
|
|
||||||
|
|
||||||
async def resolve_username(self: Client) -> Chat:
|
def get_contacts(self: Client) -> AsyncList[User]:
|
||||||
self
|
return ContactList(self)
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
def resolved_peer_to_chat(resolved: abcs.contacts.ResolvedPeer) -> Chat:
|
||||||
|
assert isinstance(resolved, types.contacts.ResolvedPeer)
|
||||||
|
|
||||||
|
map = build_chat_map(resolved.users, resolved.chats)
|
||||||
|
if chat := map.get(peer_id(resolved.peer)):
|
||||||
|
return chat
|
||||||
|
else:
|
||||||
|
raise ValueError(f"no matching chat found in response")
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_phone(client: Client, phone: str) -> Chat:
|
||||||
|
return resolved_peer_to_chat(
|
||||||
|
await client(functions.contacts.resolve_phone(phone=phone))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_username(self: Client, username: str) -> Chat:
|
||||||
|
return resolved_peer_to_chat(
|
||||||
|
await self(functions.contacts.resolve_username(username=username))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
||||||
|
@ -68,6 +108,24 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("unexpected case")
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
if isinstance(chat, str):
|
||||||
|
if chat.startswith("+"):
|
||||||
|
resolved = await resolve_phone(self, chat)
|
||||||
|
elif chat == "me":
|
||||||
|
if me := self._config.session.user:
|
||||||
|
return PackedChat(
|
||||||
|
ty=PackedType.BOT if me.bot else PackedType.USER,
|
||||||
|
id=me.id,
|
||||||
|
access_hash=0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
resolved = None
|
||||||
|
else:
|
||||||
|
resolved = await resolve_username(self, username=chat)
|
||||||
|
|
||||||
|
if resolved and (packed := resolved.pack()) is not None:
|
||||||
|
return packed
|
||||||
|
|
||||||
raise ValueError("Cannot resolve chat")
|
raise ValueError("Cannot resolve chat")
|
||||||
|
|
||||||
|
|
||||||
|
|
65
client/src/telethon/_impl/client/errors.py
Normal file
65
client/src/telethon/_impl/client/errors.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import re
|
||||||
|
from typing import Dict, Tuple, Type
|
||||||
|
|
||||||
|
from ..mtproto import RpcError
|
||||||
|
|
||||||
|
|
||||||
|
def canonicalize_code(code: int) -> int:
|
||||||
|
return abs(code) # -503 Timeout -> 503
|
||||||
|
|
||||||
|
|
||||||
|
def canonicalize_name(name: str) -> str:
|
||||||
|
return name.upper() # -503 Timeout -> TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
|
def adapt_user_name(name: str) -> str:
|
||||||
|
return re.sub(r"[A-Z]", lambda m: "_" + m[0].lower(), name).strip("_").upper()
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_name(name: str) -> str:
|
||||||
|
return "".join(map(str.title, name.split("_")))
|
||||||
|
|
||||||
|
|
||||||
|
def from_code(code: int, *, _cache: Dict[int, Type[RpcError]] = {}) -> Type[RpcError]:
|
||||||
|
code = canonicalize_code(code)
|
||||||
|
if code not in _cache:
|
||||||
|
_cache[code] = type(f"Code{code}", (RpcError,), {})
|
||||||
|
return _cache[code]
|
||||||
|
|
||||||
|
|
||||||
|
def from_name(name: str, *, _cache: Dict[str, Type[RpcError]] = {}) -> Type[RpcError]:
|
||||||
|
name = canonicalize_name(name)
|
||||||
|
if name not in _cache:
|
||||||
|
_cache[name] = type(pretty_name(name), (RpcError,), {})
|
||||||
|
return _cache[name]
|
||||||
|
|
||||||
|
|
||||||
|
def adapt_rpc(
|
||||||
|
error: RpcError, *, _cache: Dict[Tuple[int, str], Type[RpcError]] = {}
|
||||||
|
) -> RpcError:
|
||||||
|
code = canonicalize_code(error.code)
|
||||||
|
name = canonicalize_name(error.name)
|
||||||
|
tup = code, name
|
||||||
|
if tup not in _cache:
|
||||||
|
_cache[tup] = type(pretty_name(name), (from_code(code), from_name(name)), {})
|
||||||
|
return _cache[tup](
|
||||||
|
code=error.code, name=error.name, value=error.value, caused_by=error._caused_by
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorFactory:
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> Type[RpcError]:
|
||||||
|
if m := re.match(r"Code(\d+)$", name):
|
||||||
|
return from_code(int(m[1]))
|
||||||
|
else:
|
||||||
|
adapted = adapt_user_name(name)
|
||||||
|
if pretty_name(canonicalize_name(adapted)) != name or re.match(
|
||||||
|
r"[A-Z]{2}", name
|
||||||
|
):
|
||||||
|
raise AttributeError(f"error subclass names must be CamelCase: {name}")
|
||||||
|
return from_name(adapted)
|
||||||
|
|
||||||
|
|
||||||
|
errors = ErrorFactory()
|
|
@ -1,10 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
from typing import TYPE_CHECKING, Optional, Self
|
from typing import TYPE_CHECKING, Dict, Optional, Self
|
||||||
|
|
||||||
from ...tl import abcs
|
from ...tl import abcs
|
||||||
from ..types.meta import NoPublicConstructor
|
from ..types import Chat, NoPublicConstructor
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..client.client import Client
|
from ..client.client import Client
|
||||||
|
@ -17,5 +17,7 @@ class Event(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
|
def _try_from_update(
|
||||||
|
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||||
|
) -> Optional[Self]:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Callable, Literal, Sequence, Tuple, Union
|
from typing import Callable, Literal, Sequence, Tuple, Type, Union
|
||||||
|
|
||||||
from ...types import Channel, Group, User
|
from ...types import Channel, Group, User
|
||||||
from ..event import Event
|
from ..event import Event
|
||||||
|
@ -66,7 +66,7 @@ class ChatType:
|
||||||
type: Union[Literal["user"], Literal["group"], Literal["broadcast"]],
|
type: Union[Literal["user"], Literal["group"], Literal["broadcast"]],
|
||||||
) -> None:
|
) -> None:
|
||||||
if type == "user":
|
if type == "user":
|
||||||
self._type: Union[User, Group, Channel] = User
|
self._type: Union[Type[User], Type[Group], Type[Channel]] = User
|
||||||
elif type == "group":
|
elif type == "group":
|
||||||
self._type = Group
|
self._type = Group
|
||||||
elif type == "broadcast":
|
elif type == "broadcast":
|
||||||
|
@ -85,6 +85,8 @@ class ChatType:
|
||||||
return "group"
|
return "group"
|
||||||
elif self._type == Channel:
|
elif self._type == Channel:
|
||||||
return "broadcast"
|
return "broadcast"
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
def __call__(self, event: Event) -> bool:
|
def __call__(self, event: Event) -> bool:
|
||||||
sender = getattr(event, "chat", None)
|
sender = getattr(event, "chat", None)
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
from ..event import Event
|
from ..event import Event
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ...client import Client
|
||||||
|
|
||||||
|
|
||||||
class Text:
|
class Text:
|
||||||
"""
|
"""
|
||||||
|
@ -34,17 +39,46 @@ class Command:
|
||||||
filter ``Command('/help')`` will match both ``"/help"`` and ``"/help@bot"``, but not
|
filter ``Command('/help')`` will match both ``"/help"`` and ``"/help@bot"``, but not
|
||||||
``"/list"`` or ``"/help@other"``.
|
``"/list"`` or ``"/help@other"``.
|
||||||
|
|
||||||
Note that the leading forward-slash is not automatically added,
|
.. note::
|
||||||
which allows for using a different prefix or no prefix at all.
|
|
||||||
|
The leading forward-slash is not automatically added!
|
||||||
|
This allows for using a different prefix or no prefix at all.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The username is taken from the :term:`session` to avoid network calls.
|
||||||
|
If a custom storage returns the incorrect username, the filter will misbehave.
|
||||||
|
If there is no username, then the ``"/help@other"`` syntax will be ignored.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_cmd",)
|
__slots__ = ("_cmd", "_username")
|
||||||
|
|
||||||
def __init__(self, command: str) -> None:
|
def __init__(self, command: str) -> None:
|
||||||
|
if re.match(r"\s", command):
|
||||||
|
raise ValueError(f"command cannot contain spaces: {command}")
|
||||||
|
|
||||||
self._cmd = command
|
self._cmd = command
|
||||||
|
self._username: Optional[str] = None
|
||||||
|
|
||||||
def __call__(self, event: Event) -> bool:
|
def __call__(self, event: Event) -> bool:
|
||||||
raise NotImplementedError
|
text: Optional[str] = getattr(event, "text", None)
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._username is None:
|
||||||
|
self._username = ""
|
||||||
|
client: Optional[Client]
|
||||||
|
if (client := getattr(event, "_client", None)) is not None:
|
||||||
|
user = client._config.session.user
|
||||||
|
if user and user.username:
|
||||||
|
self._username = user.username
|
||||||
|
|
||||||
|
cmd = text.split(maxsplit=1)[0]
|
||||||
|
if cmd == self._cmd:
|
||||||
|
return True
|
||||||
|
if self._username:
|
||||||
|
return cmd == f"{self._cmd}@{self._username}"
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Incoming:
|
class Incoming:
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional, Self
|
from typing import TYPE_CHECKING, Dict, List, Optional, Self
|
||||||
|
|
||||||
from ...session.message_box.adaptor import (
|
|
||||||
update_short_chat_message,
|
|
||||||
update_short_message,
|
|
||||||
)
|
|
||||||
from ...tl import abcs, types
|
from ...tl import abcs, types
|
||||||
from ..types import Message
|
from ..types import Chat, Message
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -26,10 +22,12 @@ class NewMessage(Event, Message):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
|
def _try_from_update(
|
||||||
|
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||||
|
) -> Optional[Self]:
|
||||||
if isinstance(update, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
|
if isinstance(update, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
|
||||||
if isinstance(update.message, types.Message):
|
if isinstance(update.message, types.Message):
|
||||||
return cls._from_raw(update.message)
|
return cls._from_raw(client, update.message, chat_map)
|
||||||
elif isinstance(
|
elif isinstance(
|
||||||
update, (types.UpdateShortMessage, types.UpdateShortChatMessage)
|
update, (types.UpdateShortMessage, types.UpdateShortChatMessage)
|
||||||
):
|
):
|
||||||
|
@ -38,24 +36,49 @@ class NewMessage(Event, Message):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class MessageEdited(Event):
|
class MessageEdited(Event, Message):
|
||||||
"""
|
"""
|
||||||
Occurs when a new message is sent or received.
|
Occurs when a new message is sent or received.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
|
def _try_from_update(
|
||||||
raise NotImplementedError()
|
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
if isinstance(
|
||||||
|
update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)
|
||||||
|
):
|
||||||
|
return cls._from_raw(client, update.message, chat_map)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class MessageDeleted(Event):
|
class MessageDeleted(Event):
|
||||||
"""
|
"""
|
||||||
Occurs when one or more messages are deleted.
|
Occurs when one or more messages are deleted.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Telegram does not send the contents of the deleted messages.
|
||||||
|
Because they are deleted, it's also impossible to fetch them.
|
||||||
|
|
||||||
|
The chat is only known when the deletion occurs in broadcast channels or supergroups.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, msg_ids: List[int], channel_id: Optional[int]) -> None:
|
||||||
|
self._msg_ids = msg_ids
|
||||||
|
self._channel_id = channel_id
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
|
def _try_from_update(
|
||||||
raise NotImplementedError()
|
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
if isinstance(update, types.UpdateDeleteMessages):
|
||||||
|
return cls._create(update.messages, None)
|
||||||
|
elif isinstance(update, types.UpdateDeleteChannelMessages):
|
||||||
|
return cls._create(update.messages, update.channel_id)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class MessageRead(Event):
|
class MessageRead(Event):
|
||||||
|
@ -63,6 +86,26 @@ class MessageRead(Event):
|
||||||
Occurs both when your messages are read by others, and when you read messages.
|
Occurs both when your messages are read by others, and when you read messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, peer: abcs.Peer, max_id: int, out: bool) -> None:
|
||||||
|
self._peer = peer
|
||||||
|
self._max_id = max_id
|
||||||
|
self._out = out
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
|
def _try_from_update(
|
||||||
raise NotImplementedError()
|
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
if isinstance(update, types.UpdateReadHistoryInbox):
|
||||||
|
return cls._create(update.peer, update.max_id, False)
|
||||||
|
elif isinstance(update, types.UpdateReadHistoryOutbox):
|
||||||
|
return cls._create(update.peer, update.max_id, True)
|
||||||
|
elif isinstance(update, types.UpdateReadChannelInbox):
|
||||||
|
return cls._create(
|
||||||
|
types.PeerChannel(channel_id=update.channel_id), update.max_id, False
|
||||||
|
)
|
||||||
|
elif isinstance(update, types.UpdateReadChannelOutbox):
|
||||||
|
return cls._create(
|
||||||
|
types.PeerChannel(channel_id=update.channel_id), update.max_id, True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional, Self
|
from typing import TYPE_CHECKING, Dict, Optional, Self
|
||||||
|
|
||||||
from ...tl import abcs
|
from ...tl import abcs, types
|
||||||
|
from ..types import Chat
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -16,9 +17,17 @@ class CallbackQuery(Event):
|
||||||
Only bot accounts can receive this event.
|
Only bot accounts can receive this event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, update: types.UpdateBotCallbackQuery):
|
||||||
|
self._update = update
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
|
def _try_from_update(
|
||||||
raise NotImplementedError()
|
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
if isinstance(update, types.UpdateBotCallbackQuery):
|
||||||
|
return cls._create(update)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class InlineQuery(Event):
|
class InlineQuery(Event):
|
||||||
|
@ -28,6 +37,14 @@ class InlineQuery(Event):
|
||||||
Only bot accounts can receive this event.
|
Only bot accounts can receive this event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, update: types.UpdateBotInlineQuery):
|
||||||
|
self._update = update
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
|
def _try_from_update(
|
||||||
raise NotImplementedError()
|
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
if isinstance(update, types.UpdateBotInlineQuery):
|
||||||
|
return cls._create(update)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from .async_list import AsyncList
|
from .async_list import AsyncList
|
||||||
from .chat import Channel, Chat, ChatLike, Group, RestrictionReason, User
|
from .chat import Channel, Chat, ChatLike, Group, RestrictionReason, User
|
||||||
from .file import File, InFileLike, MediaLike, OutFileLike
|
from .dialog import Dialog
|
||||||
|
from .file import File, InFileLike, InWrapper, OutFileLike, OutWrapper
|
||||||
from .login_token import LoginToken
|
from .login_token import LoginToken
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .meta import NoPublicConstructor
|
from .meta import NoPublicConstructor
|
||||||
|
from .participant import Participant
|
||||||
from .password_token import PasswordToken
|
from .password_token import PasswordToken
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -14,12 +16,15 @@ __all__ = [
|
||||||
"Group",
|
"Group",
|
||||||
"RestrictionReason",
|
"RestrictionReason",
|
||||||
"User",
|
"User",
|
||||||
|
"Dialog",
|
||||||
"File",
|
"File",
|
||||||
"InFileLike",
|
"InFileLike",
|
||||||
"MediaLike",
|
"InWrapper",
|
||||||
"OutFileLike",
|
"OutFileLike",
|
||||||
|
"OutWrapper",
|
||||||
"LoginToken",
|
"LoginToken",
|
||||||
"Message",
|
"Message",
|
||||||
"NoPublicConstructor",
|
"NoPublicConstructor",
|
||||||
|
"Participant",
|
||||||
"PasswordToken",
|
"PasswordToken",
|
||||||
]
|
]
|
||||||
|
|
26
client/src/telethon/_impl/client/types/dialog.py
Normal file
26
client/src/telethon/_impl/client/types/dialog.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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 Dialog(metaclass=NoPublicConstructor):
|
||||||
|
"""
|
||||||
|
A dialog.
|
||||||
|
|
||||||
|
This represents an open conversation your chat list.
|
||||||
|
|
||||||
|
This includes the groups you've joined, channels you've subscribed to, and open one-to-one private conversations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_raw", "_chat_map")
|
||||||
|
|
||||||
|
def __init__(self, raw: abcs.Dialog, chat_map: Dict[int, Chat]) -> None:
|
||||||
|
self._raw = raw
|
||||||
|
self._chat_map = chat_map
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_raw(cls, dialog: abcs.Dialog, chat_map: Dict[int, Chat]) -> Self:
|
||||||
|
return cls._create(dialog, chat_map)
|
|
@ -1,7 +1,10 @@
|
||||||
import os
|
import os
|
||||||
|
from inspect import isawaitable
|
||||||
|
from io import BufferedReader, BufferedWriter
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Coroutine, List, Optional, Protocol, Self, Union
|
from types import TracebackType
|
||||||
|
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
|
||||||
|
@ -29,9 +32,6 @@ def photo_size_byte_count(size: abcs.PhotoSize) -> int:
|
||||||
raise RuntimeError("unexpected case")
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
|
||||||
MediaLike = object
|
|
||||||
|
|
||||||
|
|
||||||
class InFileLike(Protocol):
|
class InFileLike(Protocol):
|
||||||
"""
|
"""
|
||||||
A :term:`file-like object` used for input only.
|
A :term:`file-like object` used for input only.
|
||||||
|
@ -52,6 +52,57 @@ class OutFileLike(Protocol):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InWrapper:
|
||||||
|
__slots__ = ("_fd", "_owned")
|
||||||
|
|
||||||
|
def __init__(self, file: Union[str, Path, InFileLike]):
|
||||||
|
if isinstance(file, str):
|
||||||
|
file = Path(file)
|
||||||
|
|
||||||
|
if isinstance(file, Path):
|
||||||
|
self._fd: Union[InFileLike, BufferedReader] = file.open("rb")
|
||||||
|
self._owned = True
|
||||||
|
else:
|
||||||
|
self._fd = file
|
||||||
|
self._owned = False
|
||||||
|
|
||||||
|
async def read(self, n: int) -> bytes:
|
||||||
|
ret = self._fd.read(n)
|
||||||
|
chunk = await ret if isawaitable(ret) else ret
|
||||||
|
assert isinstance(chunk, bytes)
|
||||||
|
return chunk
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self._owned:
|
||||||
|
assert hasattr(self._fd, "close")
|
||||||
|
self._fd.close()
|
||||||
|
|
||||||
|
|
||||||
|
class OutWrapper:
|
||||||
|
__slots__ = ("_fd", "_owned")
|
||||||
|
|
||||||
|
def __init__(self, file: Union[str, Path, OutFileLike]):
|
||||||
|
if isinstance(file, str):
|
||||||
|
file = Path(file)
|
||||||
|
|
||||||
|
if isinstance(file, Path):
|
||||||
|
self._fd: Union[OutFileLike, BufferedWriter] = file.open("wb")
|
||||||
|
self._owned = True
|
||||||
|
else:
|
||||||
|
self._fd = file
|
||||||
|
self._owned = False
|
||||||
|
|
||||||
|
async def write(self, chunk: bytes) -> None:
|
||||||
|
ret = self._fd.write(chunk)
|
||||||
|
if isawaitable(ret):
|
||||||
|
await ret
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self._owned:
|
||||||
|
assert hasattr(self._fd, "close")
|
||||||
|
self._fd.close()
|
||||||
|
|
||||||
|
|
||||||
class File(metaclass=NoPublicConstructor):
|
class File(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
File information of uploaded media.
|
File information of uploaded media.
|
||||||
|
@ -264,8 +315,6 @@ class File(metaclass=NoPublicConstructor):
|
||||||
preload_prefix_size=None,
|
preload_prefix_size=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
raise NotImplementedError("sticker")
|
|
||||||
|
|
||||||
photo = compress and mime_type.startswith("image/")
|
photo = compress and mime_type.startswith("image/")
|
||||||
|
|
||||||
|
@ -296,8 +345,37 @@ class File(metaclass=NoPublicConstructor):
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ext(self):
|
def ext(self) -> str:
|
||||||
raise NotImplementedError
|
return self._path.suffix if self._path else ""
|
||||||
|
|
||||||
async def _read(self, n: int) -> bytes:
|
def _open(self) -> InWrapper:
|
||||||
raise NotImplementedError
|
file = self._file or self._path
|
||||||
|
if file is None:
|
||||||
|
raise TypeError(f"cannot use file for uploading: {self}")
|
||||||
|
return InWrapper(file)
|
||||||
|
|
||||||
|
def _input_location(self) -> abcs.InputFileLocation:
|
||||||
|
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="",
|
||||||
|
)
|
||||||
|
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")
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise TypeError(f"cannot use file for downloading: {self}")
|
||||||
|
|
|
@ -1,28 +1,41 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Optional, Self
|
from typing import TYPE_CHECKING, Dict, Optional, Self
|
||||||
|
|
||||||
from ...tl import abcs, types
|
from ...tl import abcs, types
|
||||||
from .chat import Chat
|
from ..parsers import generate_html_message, generate_markdown_message
|
||||||
|
from ..utils import expand_peer, peer_id
|
||||||
|
from .chat import Chat, ChatLike
|
||||||
from .file import File
|
from .file import File
|
||||||
from .meta import NoPublicConstructor
|
from .meta import NoPublicConstructor
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..client import Client
|
||||||
|
|
||||||
|
|
||||||
class Message(metaclass=NoPublicConstructor):
|
class Message(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
A sent message.
|
A sent message.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_raw",)
|
__slots__ = ("_client", "_raw", "_chat_map")
|
||||||
|
|
||||||
def __init__(self, message: abcs.Message) -> None:
|
def __init__(
|
||||||
|
self, client: Client, message: abcs.Message, chat_map: Dict[int, Chat]
|
||||||
|
) -> None:
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
message, (types.Message, types.MessageService, types.MessageEmpty)
|
message, (types.Message, types.MessageService, types.MessageEmpty)
|
||||||
)
|
)
|
||||||
|
self._client = client
|
||||||
self._raw = message
|
self._raw = message
|
||||||
|
self._chat_map = chat_map
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_raw(cls, message: abcs.Message) -> Self:
|
def _from_raw(
|
||||||
return cls._create(message)
|
cls, client: Client, message: abcs.Message, chat_map: Dict[int, Chat]
|
||||||
|
) -> Self:
|
||||||
|
return cls._create(client, message, chat_map)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> int:
|
def id(self) -> int:
|
||||||
|
@ -34,11 +47,21 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text_html(self) -> Optional[str]:
|
def text_html(self) -> Optional[str]:
|
||||||
raise NotImplementedError
|
if text := getattr(self._raw, "message", None):
|
||||||
|
return generate_html_message(
|
||||||
|
text, getattr(self._raw, "entities", None) or []
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text_markdown(self) -> Optional[str]:
|
def text_markdown(self) -> Optional[str]:
|
||||||
raise NotImplementedError
|
if text := getattr(self._raw, "message", None):
|
||||||
|
return generate_markdown_message(
|
||||||
|
text, getattr(self._raw, "entities", None) or []
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date(self) -> Optional[datetime.datetime]:
|
def date(self) -> Optional[datetime.datetime]:
|
||||||
|
@ -51,11 +74,20 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chat(self) -> Chat:
|
def chat(self) -> Chat:
|
||||||
raise NotImplementedError
|
peer = self._raw.peer_id or types.PeerUser(user_id=0)
|
||||||
|
broadcast = broadcast = getattr(self._raw, "post", None)
|
||||||
|
return self._chat_map.get(peer_id(peer)) or expand_peer(
|
||||||
|
peer, broadcast=broadcast
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sender(self) -> Chat:
|
def sender(self) -> Optional[Chat]:
|
||||||
raise NotImplementedError
|
if (from_ := getattr(self._raw, "from_id", None)) is not None:
|
||||||
|
return self._chat_map.get(peer_id(from_)) or expand_peer(
|
||||||
|
from_, broadcast=getattr(self._raw, "post", None)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def _file(self) -> Optional[File]:
|
def _file(self) -> Optional[File]:
|
||||||
return (
|
return (
|
||||||
|
@ -97,11 +129,39 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
def file(self) -> Optional[File]:
|
def file(self) -> Optional[File]:
|
||||||
return self._file()
|
return self._file()
|
||||||
|
|
||||||
async def delete(self) -> None:
|
async def delete(self, *, revoke: bool = True) -> int:
|
||||||
raise NotImplementedError
|
"""
|
||||||
|
Alias for :meth:`telethon.Client.delete_messages`.
|
||||||
|
|
||||||
async def edit(self) -> None:
|
See the documentation of :meth:`~telethon.Client.delete_messages` for an explanation of the parameters.
|
||||||
raise NotImplementedError
|
"""
|
||||||
|
return await self._client.delete_messages(self.chat, [self.id], revoke=revoke)
|
||||||
|
|
||||||
async def forward_to(self) -> None:
|
async def edit(
|
||||||
raise NotImplementedError
|
self,
|
||||||
|
text: Optional[str] = None,
|
||||||
|
markdown: Optional[str] = None,
|
||||||
|
html: Optional[str] = None,
|
||||||
|
link_preview: Optional[bool] = None,
|
||||||
|
) -> Message:
|
||||||
|
"""
|
||||||
|
Alias for :meth:`telethon.Client.edit_message`.
|
||||||
|
|
||||||
|
See the documentation of :meth:`~telethon.Client.edit_message` for an explanation of the parameters.
|
||||||
|
"""
|
||||||
|
return await self._client.edit_message(
|
||||||
|
self.chat,
|
||||||
|
self.id,
|
||||||
|
text=text,
|
||||||
|
markdown=markdown,
|
||||||
|
html=html,
|
||||||
|
link_preview=link_preview,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def forward_to(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]
|
||||||
|
|
67
client/src/telethon/_impl/client/types/participant.py
Normal file
67
client/src/telethon/_impl/client/types/participant.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
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 Participant(metaclass=NoPublicConstructor):
|
||||||
|
"""
|
||||||
|
A participant in a chat, including the corresponding user and permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_raw", "_chat_map")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
participant: Union[
|
||||||
|
types.ChannelParticipant,
|
||||||
|
types.ChannelParticipantSelf,
|
||||||
|
types.ChannelParticipantCreator,
|
||||||
|
types.ChannelParticipantAdmin,
|
||||||
|
types.ChannelParticipantBanned,
|
||||||
|
types.ChannelParticipantLeft,
|
||||||
|
types.ChatParticipant,
|
||||||
|
types.ChatParticipantCreator,
|
||||||
|
types.ChatParticipantAdmin,
|
||||||
|
],
|
||||||
|
chat_map: Dict[int, Chat],
|
||||||
|
) -> None:
|
||||||
|
self._raw = participant
|
||||||
|
self._chat_map = chat_map
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_raw_channel(
|
||||||
|
cls, participant: abcs.ChannelParticipant, chat_map: Dict[int, Chat]
|
||||||
|
) -> Self:
|
||||||
|
if isinstance(
|
||||||
|
participant,
|
||||||
|
(
|
||||||
|
types.ChannelParticipant,
|
||||||
|
types.ChannelParticipantSelf,
|
||||||
|
types.ChannelParticipantCreator,
|
||||||
|
types.ChannelParticipantAdmin,
|
||||||
|
types.ChannelParticipantBanned,
|
||||||
|
types.ChannelParticipantLeft,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
return cls._create(participant, chat_map)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_raw_chat(
|
||||||
|
cls, participant: abcs.ChatParticipant, chat_map: Dict[int, Chat]
|
||||||
|
) -> Self:
|
||||||
|
if isinstance(
|
||||||
|
participant,
|
||||||
|
(
|
||||||
|
types.ChatParticipant,
|
||||||
|
types.ChatParticipantCreator,
|
||||||
|
types.ChatParticipantAdmin,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
return cls._create(participant, chat_map)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected case")
|
|
@ -1,4 +1,21 @@
|
||||||
|
import itertools
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
DefaultDict,
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..tl import abcs, types
|
||||||
|
from .types import Channel, Chat, Group, User
|
||||||
|
|
||||||
_last_id = 0
|
_last_id = 0
|
||||||
|
|
||||||
|
@ -9,3 +26,74 @@ def generate_random_id() -> int:
|
||||||
_last_id = int(time.time() * 1e9)
|
_last_id = int(time.time() * 1e9)
|
||||||
_last_id += 1
|
_last_id += 1
|
||||||
return _last_id
|
return _last_id
|
||||||
|
|
||||||
|
|
||||||
|
def build_chat_map(users: List[abcs.User], chats: List[abcs.Chat]) -> Dict[int, Chat]:
|
||||||
|
users_iter = (User._from_raw(u) for u in users)
|
||||||
|
chats_iter = (
|
||||||
|
Channel._from_raw(c)
|
||||||
|
if isinstance(c, (types.Channel, types.ChannelForbidden)) and c.broadcast
|
||||||
|
else Group._from_raw(c)
|
||||||
|
for c in chats
|
||||||
|
)
|
||||||
|
|
||||||
|
# https://github.com/python/mypy/issues/2115
|
||||||
|
result: Dict[int, Chat] = {
|
||||||
|
c.id: c for c in itertools.chain(users_iter, chats_iter) # type: ignore [attr-defined, misc]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) != len(users) + len(chats):
|
||||||
|
# The fabled ID collision between different chat types.
|
||||||
|
counter: DefaultDict[int, List[Union[abcs.User, abcs.Chat]]] = defaultdict(list)
|
||||||
|
for user in users:
|
||||||
|
if (id := getattr(user, "id", None)) is not None:
|
||||||
|
counter[id].append(user)
|
||||||
|
for chat in chats:
|
||||||
|
if (id := getattr(chat, "id", None)) is not None:
|
||||||
|
counter[id].append(chat)
|
||||||
|
|
||||||
|
for k, v in counter.items():
|
||||||
|
if len(v) > 1:
|
||||||
|
# TODO proper logger
|
||||||
|
for x in v:
|
||||||
|
print(x, file=sys.stderr)
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
f"chat identifier collision: {k}; please report this"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def peer_id(peer: abcs.Peer) -> int:
|
||||||
|
if isinstance(peer, types.PeerUser):
|
||||||
|
return peer.user_id
|
||||||
|
elif isinstance(peer, types.PeerChat):
|
||||||
|
return peer.chat_id
|
||||||
|
elif isinstance(peer, types.PeerChannel):
|
||||||
|
return peer.channel_id
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
|
||||||
|
def expand_peer(peer: abcs.Peer, *, broadcast: Optional[bool]) -> Chat:
|
||||||
|
if isinstance(peer, types.PeerUser):
|
||||||
|
return User._from_raw(types.UserEmpty(id=peer.user_id))
|
||||||
|
elif isinstance(peer, types.PeerChat):
|
||||||
|
return Group._from_raw(types.ChatEmpty(id=peer.chat_id))
|
||||||
|
elif isinstance(peer, types.PeerChannel):
|
||||||
|
if broadcast is None:
|
||||||
|
broadcast = True # assume broadcast by default (Channel type is more accurate than Group)
|
||||||
|
|
||||||
|
channel = types.ChannelForbidden(
|
||||||
|
broadcast=broadcast,
|
||||||
|
megagroup=not broadcast,
|
||||||
|
id=peer.channel_id,
|
||||||
|
access_hash=0,
|
||||||
|
title="",
|
||||||
|
until_date=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Channel._from_raw(channel) if broadcast else Group._from_raw(channel)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected case")
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
class ErrorFactory:
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __getattribute__(self, name: str) -> ValueError:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
errors = ErrorFactory()
|
|
|
@ -30,7 +30,7 @@ class RpcError(ValueError):
|
||||||
value: Optional[int] = None,
|
value: Optional[int] = None,
|
||||||
caused_by: Optional[int] = None,
|
caused_by: Optional[int] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
append_value = f" ({value})" if value else None
|
append_value = f" ({value})" if value else ""
|
||||||
super().__init__(f"rpc error {code}: {name}{append_value}")
|
super().__init__(f"rpc error {code}: {name}{append_value}")
|
||||||
|
|
||||||
self._code = code
|
self._code = code
|
||||||
|
|
|
@ -38,7 +38,9 @@ class SqliteSession(Storage):
|
||||||
if version == 7:
|
if version == 7:
|
||||||
session = self._load_v7(c)
|
session = self._load_v7(c)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise ValueError(
|
||||||
|
"only migration from sqlite session format 7 supported"
|
||||||
|
)
|
||||||
|
|
||||||
self._reset(c)
|
self._reset(c)
|
||||||
self._get_or_init_version(c)
|
self._get_or_init_version(c)
|
||||||
|
|
|
@ -4,13 +4,14 @@ from ._impl.client.types import (
|
||||||
Channel,
|
Channel,
|
||||||
Chat,
|
Chat,
|
||||||
ChatLike,
|
ChatLike,
|
||||||
|
Dialog,
|
||||||
File,
|
File,
|
||||||
Group,
|
Group,
|
||||||
InFileLike,
|
InFileLike,
|
||||||
LoginToken,
|
LoginToken,
|
||||||
MediaLike,
|
|
||||||
Message,
|
Message,
|
||||||
OutFileLike,
|
OutFileLike,
|
||||||
|
Participant,
|
||||||
PasswordToken,
|
PasswordToken,
|
||||||
RestrictionReason,
|
RestrictionReason,
|
||||||
User,
|
User,
|
||||||
|
@ -24,13 +25,14 @@ __all__ = [
|
||||||
"Channel",
|
"Channel",
|
||||||
"Chat",
|
"Chat",
|
||||||
"ChatLike",
|
"ChatLike",
|
||||||
|
"Dialog",
|
||||||
"File",
|
"File",
|
||||||
"Group",
|
"Group",
|
||||||
"InFileLike",
|
"InFileLike",
|
||||||
"LoginToken",
|
"LoginToken",
|
||||||
"MediaLike",
|
|
||||||
"Message",
|
"Message",
|
||||||
"OutFileLike",
|
"OutFileLike",
|
||||||
|
"Participant",
|
||||||
"PasswordToken",
|
"PasswordToken",
|
||||||
"RestrictionReason",
|
"RestrictionReason",
|
||||||
"User",
|
"User",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user