Add missing implementations

This commit is contained in:
Lonami Exo 2023-09-17 22:54:52 +02:00
parent 5f4470dd57
commit 6a880f1ff1
26 changed files with 1058 additions and 205 deletions

View File

@ -1,6 +1,6 @@
from ._impl import tl as _tl
from ._impl.client import Client, Config
from ._impl.errors import errors
from ._impl.client.errors import errors
from ._impl.mtproto import RpcError
from ._impl.session import Session
from .version import __version__

View File

@ -4,8 +4,8 @@ import getpass
import re
from typing import TYPE_CHECKING, Optional, Union
from ...crypto import two_factor_auth
from ...mtproto import RpcError
from ...session import Session
from ...session import User as SessionUser
from ...tl import abcs, functions, types
from ..types import LoginToken, PasswordToken, User
@ -192,8 +192,46 @@ async def get_password_information(client: Client) -> PasswordToken:
async def check_password(
self: Client, token: PasswordToken, password: Union[str, bytes]
) -> User:
self, token, password
raise NotImplementedError
algo = token._password.current_algo
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:

View File

@ -1,11 +1,83 @@
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:
from .client import Client
def get_participants(self: Client) -> None:
self
raise NotImplementedError
class ParticipantList(AsyncList[Participant]):
def __init__(
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)

View File

@ -35,12 +35,13 @@ from ..types import (
AsyncList,
Chat,
ChatLike,
Dialog,
File,
InFileLike,
LoginToken,
MediaLike,
Message,
OutFileLike,
Participant,
PasswordToken,
User,
)
@ -58,7 +59,7 @@ from .chats import get_participants
from .dialogs import delete_dialog, get_dialogs
from .files import (
download,
iter_download,
get_file_bytes,
send_audio,
send_file,
send_photo,
@ -184,7 +185,7 @@ class Client:
self._chat_hashes = ChatHashCache(None)
self._last_update_limit_warn: Optional[float] = None
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)
self._dispatcher: Optional[asyncio.Task[None]] = None
self._downloader_map = object()
@ -315,8 +316,29 @@ class Client:
"""
await connect(self)
async def delete_dialog(self) -> None:
await delete_dialog(self)
async def delete_dialog(self, chat: ChatLike) -> None:
"""
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(
self, chat: ChatLike, message_ids: List[int], *, revoke: bool = True
@ -374,12 +396,12 @@ class Client:
"""
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.
:param media:
The media to download.
The media file to download.
This will often come from :attr:`telethon.types.Message.file`.
:param file:
@ -387,6 +409,10 @@ class Client:
Note that the extension is not automatically added to the path.
You can get the file extension with :attr:`telethon.types.File.ext`.
.. warning::
If the file already exists, it will be overwritten!
.. rubric:: Example
.. code-block:: python
@ -400,7 +426,7 @@ class Client:
.. 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)
@ -485,7 +511,7 @@ class Client:
"""
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.
@ -498,10 +524,29 @@ class Client:
async for user in client.get_contacts():
print(user.full_name, user.id)
"""
return await get_contacts(self)
return get_contacts(self)
def get_dialogs(self) -> None:
get_dialogs(self)
def get_dialogs(self) -> AsyncList[Dialog]:
"""
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(
self, handler: Callable[[Event], Awaitable[Any]]
@ -564,8 +609,6 @@ class Client:
"""
Get the message history from a :term:`chat`.
Edit a message.
:param chat:
The :term:`chat` where the message to edit is.
@ -604,8 +647,24 @@ class Client:
) -> AsyncList[Message]:
return get_messages_with_ids(self, chat, message_ids)
def get_participants(self) -> None:
get_participants(self)
def get_participants(self, chat: ChatLike) -> AsyncList[Participant]:
"""
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(
self, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
@ -674,8 +733,31 @@ class Client:
"""
return await is_authorized(self)
async def iter_download(self) -> None:
await iter_download(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 on(
self, event_cls: Type[Event], filter: Optional[Filter] = None
@ -815,8 +897,8 @@ class Client:
"""
return await resolve_to_packed(self, chat)
async def resolve_username(self) -> Chat:
return await resolve_username(self)
async def resolve_username(self, username: str) -> Chat:
return await resolve_username(self, username)
async def run_until_disconnected(self) -> None:
await run_until_disconnected(self)

View File

@ -2,15 +2,79 @@ from __future__ import annotations
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:
from .client import Client
def get_dialogs(self: Client) -> None:
self
raise NotImplementedError
class DialogList(AsyncList[Dialog]):
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_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:
self
raise NotImplementedError
def get_dialogs(self: Client) -> AsyncList[Dialog]:
return DialogList(self)
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,
)
)

View File

@ -1,11 +1,23 @@
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, Optional, Union
from typing import TYPE_CHECKING, Any, Coroutine, Optional, Tuple, Union
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 .messages import parse_message
@ -222,73 +234,102 @@ async def upload(
hash_md5 = hashlib.md5()
is_big = file._size > BIG_FILE_SIZE
while uploaded != file._size:
chunk = await file._read(MAX_CHUNK_SIZE - len(buffer))
if not chunk:
raise ValueError("unexpected end-of-file")
fd = file._open()
try:
while uploaded != file._size:
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:
to_store = chunk
else:
buffer += chunk
if len(buffer) == MAX_CHUNK_SIZE:
to_store = buffer
if len(chunk) == MAX_CHUNK_SIZE or uploaded + len(chunk) == file._size:
to_store = chunk
else:
continue
buffer += chunk
if len(buffer) == MAX_CHUNK_SIZE:
to_store = buffer
else:
continue
if is_big:
await client(
functions.upload.save_big_file_part(
file_id=file_id,
file_part=part,
file_total_parts=part,
bytes=to_store,
if is_big:
await client(
functions.upload.save_big_file_part(
file_id=file_id,
file_part=part,
file_total_parts=part,
bytes=to_store,
)
)
)
else:
await client(
functions.upload.save_file_part(
file_id=file_id, file_part=total_parts, bytes=to_store
else:
await client(
functions.upload.save_file_part(
file_id=file_id, file_part=total_parts, bytes=to_store
)
)
)
hash_md5.update(to_store)
hash_md5.update(to_store)
buffer.clear()
part += 1
buffer.clear()
part += 1
finally:
fd.close()
if file._size > BIG_FILE_SIZE:
types.InputFileBig(
return types.InputFileBig(
id=file_id,
parts=total_parts,
name=file._name,
)
else:
types.InputFile(
return types.InputFile(
id=file_id,
parts=total_parts,
name=file._name,
md5_checksum=hash_md5.hexdigest(),
)
raise NotImplementedError
async def iter_download(self: Client) -> None:
raise NotImplementedError
# result = self(
# functions.upload.get_file(
# precise=False,
# cdn_supported=False,
# location=types.InputFileLocation(),
# offset=0,
# limit=MAX_CHUNK_SIZE,
# )
# )
# assert isinstance(result, types.upload.File)
# if len(result.bytes) < MAX_CHUNK_SIZE:
# done
# else:
# offset += MAX_CHUNK_SIZE
class FileBytesList(AsyncList[bytes]):
def __init__(
self,
client: Client,
file: File,
):
super().__init__()
self._client = client
self._loc = file._input_location()
self._offset = 0
async def _fetch_next(self) -> None:
result = await self._client(
functions.upload.get_file(
precise=False,
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:
raise NotImplementedError
def get_file_bytes(self: Client, media: File) -> AsyncList[bytes]:
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()

View File

@ -7,8 +7,8 @@ from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union
from ...session import PackedChat
from ...tl import abcs, functions, types
from ..parsers import parse_html_message, parse_markdown_message
from ..types import AsyncList, ChatLike, Message
from ..utils import generate_random_id
from ..types import AsyncList, Chat, ChatLike, Message
from ..utils import build_chat_map, generate_random_id, peer_id
if TYPE_CHECKING:
from .client import Client
@ -147,30 +147,47 @@ async def forward_messages(
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):
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._done = True
return chat_map
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
return chat_map
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
return chat_map
elif isinstance(messages, types.messages.MessagesNotModified):
self._total = messages.count
return {}
else:
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(
(
m
m._raw
for m in reversed(self._buffer)
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_date = offset_date
self._done = limit <= 0
async def _fetch_next(self) -> None:
if self._peer is None:
self._peer = (
@ -213,10 +232,11 @@ class HistoryList(MessageList):
self._extend_buffer(self._client, result)
self._limit -= len(self._buffer)
self._done = not self._limit
if self._buffer:
last = self._last_non_empty_message()
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
@ -331,7 +351,7 @@ class SearchList(MessageList):
if self._buffer:
last = self._last_non_empty_message()
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
@ -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)
if self._buffer:
last = self._last_non_empty_message()
last_packed = last.chat.pack()
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
if isinstance(result, types.messages.MessagesSlice):
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(
@ -483,7 +504,9 @@ class MessageMap:
def _empty(self, id: int = 0) -> Message:
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,
peer: Optional[abcs.InputPeer],
) -> MessageMap:
if isinstance(result, types.UpdateShort):
updates = [result.update]
entities: Dict[int, object] = {}
elif isinstance(result, (types.Updates, types.UpdatesCombined)):
if isinstance(result, (types.Updates, types.UpdatesCombined)):
updates = result.updates
entities = {}
raise NotImplementedError()
chat_map = build_chat_map(result.users, result.chats)
elif isinstance(result, types.UpdateShort):
updates = [result.update]
chat_map = {}
else:
return MessageMap(client, peer, {}, {})
@ -518,14 +540,8 @@ def build_message_map(
types.UpdateNewScheduledMessage,
),
):
assert isinstance(
update.message,
(types.Message, types.MessageService, types.MessageEmpty),
)
id_to_message[update.message.id] = Message._from_raw(update.message)
elif isinstance(update, types.UpdateMessagePoll):
raise NotImplementedError()
msg = Message._from_raw(client, update.message, chat_map)
id_to_message[msg.id] = msg
return MessageMap(
client,

View File

@ -14,6 +14,7 @@ from ...mtsender import connect_with_auth
from ...session import DataCenter, Session
from ...session import User as SessionUser
from ...tl import LAYER, Request, functions
from ..errors import adapt_rpc
from .updates import dispatcher, process_socket_updates
if TYPE_CHECKING:
@ -217,7 +218,7 @@ async def invoke_request(
rx = sender.enqueue(request)
continue
else:
raise
raise adapt_rpc(e) from None
return request.deserialize_response(response)

View File

@ -19,6 +19,7 @@ from ...session import Gap
from ...tl import abcs
from ..events import Event as EventBase
from ..events.filters import Filter
from ..utils import build_chat_map
if TYPE_CHECKING:
from .client import Client
@ -107,14 +108,11 @@ def extend_update_queue(
users: List[abcs.User],
chats: List[abcs.Chat],
) -> None:
entities: Dict[int, Union[abcs.User, abcs.Chat]] = {
getattr(u, "id", None) or 0: u for u in users
}
entities.update({getattr(c, "id", None) or 0: c for c in chats})
chat_map = build_chat_map(users, chats)
for update in updates:
try:
client._updates.put_nowait((update, entities))
client._updates.put_nowait((update, chat_map))
except asyncio.QueueFull:
now = asyncio.get_running_loop().time()
if client._last_update_limit_warn is None or (
@ -128,9 +126,9 @@ def extend_update_queue(
async def dispatcher(client: Client) -> None:
while client.connected:
update, entities = await client._updates.get()
update, chat_map = await client._updates.get()
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:
if not filter or filter(event):
try:

View File

@ -2,27 +2,67 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from ...mtproto import RpcError
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 ..utils import build_chat_map, peer_id
if TYPE_CHECKING:
from .client import Client
async def get_me(self: Client) -> Optional[User]:
self
raise NotImplementedError
try:
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]:
self
raise NotImplementedError
class ContactList(AsyncList[User]):
def __init__(self, client: Client):
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:
self
raise NotImplementedError
def get_contacts(self: Client) -> AsyncList[User]:
return ContactList(self)
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:
@ -68,6 +108,24 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
else:
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")

View 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()

View File

@ -1,10 +1,10 @@
from __future__ import annotations
import abc
from typing import TYPE_CHECKING, Optional, Self
from typing import TYPE_CHECKING, Dict, Optional, Self
from ...tl import abcs
from ..types.meta import NoPublicConstructor
from ..types import Chat, NoPublicConstructor
if TYPE_CHECKING:
from ..client.client import Client
@ -17,5 +17,7 @@ class Event(metaclass=NoPublicConstructor):
@classmethod
@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

View File

@ -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 ..event import Event
@ -66,7 +66,7 @@ class ChatType:
type: Union[Literal["user"], Literal["group"], Literal["broadcast"]],
) -> None:
if type == "user":
self._type: Union[User, Group, Channel] = User
self._type: Union[Type[User], Type[Group], Type[Channel]] = User
elif type == "group":
self._type = Group
elif type == "broadcast":
@ -85,6 +85,8 @@ class ChatType:
return "group"
elif self._type == Channel:
return "broadcast"
else:
raise RuntimeError("unexpected case")
def __call__(self, event: Event) -> bool:
sender = getattr(event, "chat", None)

View File

@ -1,8 +1,13 @@
from __future__ import annotations
import re
from typing import Union
from typing import TYPE_CHECKING, Optional, Union
from ..event import Event
if TYPE_CHECKING:
from ...client import Client
class Text:
"""
@ -34,17 +39,46 @@ class Command:
filter ``Command('/help')`` will match both ``"/help"`` and ``"/help@bot"``, but not
``"/list"`` or ``"/help@other"``.
Note that the leading forward-slash is not automatically added,
which allows for using a different prefix or no prefix at all.
.. note::
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:
if re.match(r"\s", command):
raise ValueError(f"command cannot contain spaces: {command}")
self._cmd = command
self._username: Optional[str] = None
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:

View File

@ -1,13 +1,9 @@
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 ..types import Message
from ..types import Chat, Message
from .event import Event
if TYPE_CHECKING:
@ -26,10 +22,12 @@ class NewMessage(Event, Message):
"""
@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.message, types.Message):
return cls._from_raw(update.message)
return cls._from_raw(client, update.message, chat_map)
elif isinstance(
update, (types.UpdateShortMessage, types.UpdateShortChatMessage)
):
@ -38,24 +36,49 @@ class NewMessage(Event, Message):
return None
class MessageEdited(Event):
class MessageEdited(Event, Message):
"""
Occurs when a new message is sent or received.
"""
@classmethod
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
raise NotImplementedError()
def _try_from_update(
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):
"""
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
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
raise NotImplementedError()
def _try_from_update(
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):
@ -63,6 +86,26 @@ class MessageRead(Event):
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
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
raise NotImplementedError()
def _try_from_update(
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

View File

@ -1,8 +1,9 @@
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
if TYPE_CHECKING:
@ -16,9 +17,17 @@ class CallbackQuery(Event):
Only bot accounts can receive this event.
"""
def __init__(self, update: types.UpdateBotCallbackQuery):
self._update = update
@classmethod
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
raise NotImplementedError()
def _try_from_update(
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):
@ -28,6 +37,14 @@ class InlineQuery(Event):
Only bot accounts can receive this event.
"""
def __init__(self, update: types.UpdateBotInlineQuery):
self._update = update
@classmethod
def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]:
raise NotImplementedError()
def _try_from_update(
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

View File

@ -1,9 +1,11 @@
from .async_list import AsyncList
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 .message import Message
from .meta import NoPublicConstructor
from .participant import Participant
from .password_token import PasswordToken
__all__ = [
@ -14,12 +16,15 @@ __all__ = [
"Group",
"RestrictionReason",
"User",
"Dialog",
"File",
"InFileLike",
"MediaLike",
"InWrapper",
"OutFileLike",
"OutWrapper",
"LoginToken",
"Message",
"NoPublicConstructor",
"Participant",
"PasswordToken",
]

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

View File

@ -1,7 +1,10 @@
import os
from inspect import isawaitable
from io import BufferedReader, BufferedWriter
from mimetypes import guess_type
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 .meta import NoPublicConstructor
@ -29,9 +32,6 @@ def photo_size_byte_count(size: abcs.PhotoSize) -> int:
raise RuntimeError("unexpected case")
MediaLike = object
class InFileLike(Protocol):
"""
A :term:`file-like object` used for input only.
@ -52,6 +52,57 @@ class OutFileLike(Protocol):
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):
"""
File information of uploaded media.
@ -264,8 +315,6 @@ class File(metaclass=NoPublicConstructor):
preload_prefix_size=None,
)
)
else:
raise NotImplementedError("sticker")
photo = compress and mime_type.startswith("image/")
@ -296,8 +345,37 @@ class File(metaclass=NoPublicConstructor):
)
@property
def ext(self):
raise NotImplementedError
def ext(self) -> str:
return self._path.suffix if self._path else ""
async def _read(self, n: int) -> bytes:
raise NotImplementedError
def _open(self) -> InWrapper:
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}")

View File

@ -1,28 +1,41 @@
from __future__ import annotations
import datetime
from typing import Optional, Self
from typing import TYPE_CHECKING, Dict, Optional, Self
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 .meta import NoPublicConstructor
if TYPE_CHECKING:
from ..client import Client
class Message(metaclass=NoPublicConstructor):
"""
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(
message, (types.Message, types.MessageService, types.MessageEmpty)
)
self._client = client
self._raw = message
self._chat_map = chat_map
@classmethod
def _from_raw(cls, message: abcs.Message) -> Self:
return cls._create(message)
def _from_raw(
cls, client: Client, message: abcs.Message, chat_map: Dict[int, Chat]
) -> Self:
return cls._create(client, message, chat_map)
@property
def id(self) -> int:
@ -34,11 +47,21 @@ class Message(metaclass=NoPublicConstructor):
@property
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
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
def date(self) -> Optional[datetime.datetime]:
@ -51,11 +74,20 @@ class Message(metaclass=NoPublicConstructor):
@property
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
def sender(self) -> Chat:
raise NotImplementedError
def sender(self) -> Optional[Chat]:
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]:
return (
@ -97,11 +129,39 @@ class Message(metaclass=NoPublicConstructor):
def file(self) -> Optional[File]:
return self._file()
async def delete(self) -> None:
raise NotImplementedError
async def delete(self, *, revoke: bool = True) -> int:
"""
Alias for :meth:`telethon.Client.delete_messages`.
async def edit(self) -> None:
raise NotImplementedError
See the documentation of :meth:`~telethon.Client.delete_messages` for an explanation of the parameters.
"""
return await self._client.delete_messages(self.chat, [self.id], revoke=revoke)
async def forward_to(self) -> None:
raise NotImplementedError
async def edit(
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]

View 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")

View File

@ -1,4 +1,21 @@
import itertools
import sys
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
@ -9,3 +26,74 @@ def generate_random_id() -> int:
_last_id = int(time.time() * 1e9)
_last_id += 1
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")

View File

@ -1,8 +0,0 @@
class ErrorFactory:
__slots__ = ()
def __getattribute__(self, name: str) -> ValueError:
raise NotImplementedError
errors = ErrorFactory()

View File

@ -30,7 +30,7 @@ class RpcError(ValueError):
value: Optional[int] = None,
caused_by: Optional[int] = 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}")
self._code = code

View File

@ -38,7 +38,9 @@ class SqliteSession(Storage):
if version == 7:
session = self._load_v7(c)
else:
raise NotImplementedError
raise ValueError(
"only migration from sqlite session format 7 supported"
)
self._reset(c)
self._get_or_init_version(c)

View File

@ -4,13 +4,14 @@ from ._impl.client.types import (
Channel,
Chat,
ChatLike,
Dialog,
File,
Group,
InFileLike,
LoginToken,
MediaLike,
Message,
OutFileLike,
Participant,
PasswordToken,
RestrictionReason,
User,
@ -24,13 +25,14 @@ __all__ = [
"Channel",
"Chat",
"ChatLike",
"Dialog",
"File",
"Group",
"InFileLike",
"LoginToken",
"MediaLike",
"Message",
"OutFileLike",
"Participant",
"PasswordToken",
"RestrictionReason",
"User",