diff --git a/client/doc/concepts/glossary.rst b/client/doc/concepts/glossary.rst index e5b5904d..aceb5e95 100644 --- a/client/doc/concepts/glossary.rst +++ b/client/doc/concepts/glossary.rst @@ -6,10 +6,10 @@ Glossary .. glossary:: :sorted: - chat + peer A :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`. - .. seealso:: The :doc:`../concepts/chats` concept. + .. seealso:: The :doc:`../concepts/peers` concept. yourself The logged-in account, whether that represents a bot or a user with a phone number. diff --git a/client/doc/concepts/messages.rst b/client/doc/concepts/messages.rst index e0fab4ef..18567440 100644 --- a/client/doc/concepts/messages.rst +++ b/client/doc/concepts/messages.rst @@ -95,9 +95,9 @@ and instead favours more standard `HTML elements ` +:doc:`‣ Start reading Chat concept ` .. toctree:: :hidden: :caption: Concepts - concepts/chats + concepts/peers concepts/updates concepts/messages concepts/sessions diff --git a/client/doc/modules/types.rst b/client/doc/modules/types.rst index 38373805..c18ba386 100644 --- a/client/doc/modules/types.rst +++ b/client/doc/modules/types.rst @@ -118,6 +118,16 @@ Private definitions New-type wrapper around :class:`int` used as a message identifier. +.. currentmodule:: telethon._impl.session.chat.peer_ref + +.. class:: PeerIdentifier + + New-type wrapper around :class:`int` used as a message identifier. + +.. class:: PeerAuth + + New-type wrapper around :class:`int` used as a message identifier. + .. currentmodule:: telethon._impl.mtsender.sender .. autoclass:: AsyncReader diff --git a/client/src/telethon/_impl/client/client/auth.py b/client/src/telethon/_impl/client/client/auth.py index 4877c467..7220a4ec 100644 --- a/client/src/telethon/_impl/client/client/auth.py +++ b/client/src/telethon/_impl/client/client/auth.py @@ -35,9 +35,7 @@ async def complete_login(client: Client, auth: abcs.auth.Authorization) -> User: id=user.id, dc=client._sender.dc_id, bot=user.bot, username=user.username ) - packed = user.pack() - assert packed is not None - client._chat_hashes.set_self_user(packed) + client._chat_hashes.set_self_user(user.id, user.bot) try: state = await client(functions.updates.get_state()) diff --git a/client/src/telethon/_impl/client/client/bots.py b/client/src/telethon/_impl/client/client/bots.py index 08f70562..744d01ba 100644 --- a/client/src/telethon/_impl/client/client/bots.py +++ b/client/src/telethon/_impl/client/client/bots.py @@ -3,8 +3,9 @@ from __future__ import annotations from collections.abc import AsyncIterator from typing import TYPE_CHECKING, Optional, Self -from ...tl import abcs, functions, types -from ..types import ChatLike, InlineResult, NoPublicConstructor +from ...session import PeerRef, UserRef +from ...tl import functions, types +from ..types import InlineResult, NoPublicConstructor, Peer, User if TYPE_CHECKING: from .client import Client @@ -16,12 +17,12 @@ class InlineResults(metaclass=NoPublicConstructor): client: Client, bot: types.InputUser, query: str, - chat: abcs.InputPeer, + peer: Optional[PeerRef], ): self._client = client self._bot = bot self._query = query - self._peer = chat or types.InputPeerEmpty() + self._peer = peer self._offset: Optional[str] = "" self._buffer: list[InlineResult] = [] self._done = False @@ -37,7 +38,11 @@ class InlineResults(metaclass=NoPublicConstructor): result = await self._client( functions.messages.get_inline_bot_results( bot=self._bot, - peer=self._peer, + peer=( + self._peer._to_input_peer() + if self._peer + else types.InputPeerEmpty() + ), geo_point=None, query=self._query, offset=self._offset, @@ -61,13 +66,16 @@ class InlineResults(metaclass=NoPublicConstructor): async def inline_query( - self: Client, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None + self: Client, + bot: User | UserRef, + /, + query: str = "", + *, + peer: Optional[Peer | PeerRef] = None, ) -> AsyncIterator[InlineResult]: - packed_bot = await self._resolve_to_packed(bot) - packed_chat = await self._resolve_to_packed(chat) if chat else None return InlineResults._create( self, - packed_bot._to_input_user(), + bot._ref._to_input_user(), query, - packed_chat._to_input_peer() if packed_chat else types.InputPeerEmpty(), + peer._ref if peer else None, ) diff --git a/client/src/telethon/_impl/client/client/chats.py b/client/src/telethon/_impl/client/client/chats.py index 35e45cb4..9c507709 100644 --- a/client/src/telethon/_impl/client/client/chats.py +++ b/client/src/telethon/_impl/client/client/chats.py @@ -3,16 +3,19 @@ from __future__ import annotations import datetime from typing import TYPE_CHECKING, Optional, Sequence -from ...session import PeerRef -from ...tl import abcs, functions, types +from ...session import ChannelRef, GroupRef, PeerRef, UserRef +from ...tl import functions, types from ..types import ( AdminRight, AsyncList, - ChatLike, + Channel, ChatRestriction, File, + Group, Participant, + Peer, RecentAction, + User, build_chat_map, ) from .messages import SearchList @@ -25,23 +28,19 @@ class ParticipantList(AsyncList[Participant]): def __init__( self, client: Client, - chat: ChatLike, + peer: ChannelRef | GroupRef, ): super().__init__() self._client = client - self._chat = chat - self._packed: Optional[PeerRef] = None + self._peer = peer self._offset = 0 self._seen: set[int] = set() async def _fetch_next(self) -> None: - if self._packed is None: - self._packed = await self._client._resolve_to_packed(self._chat) - - if self._packed.is_channel(): + if isinstance(self._peer, ChannelRef): chanp = await self._client( functions.channels.get_participants( - channel=self._packed._to_input_channel(), + channel=self._peer._to_input_channel(), filter=types.ChannelParticipantsRecent(), offset=self._offset, limit=200, @@ -55,7 +54,7 @@ class ParticipantList(AsyncList[Participant]): seen_count = len(self._seen) for p in chanp.participants: part = Participant._from_raw_channel( - self._client, self._packed, p, chat_map + self._client, self._peer, p, chat_map ) pid = part._peer_id() if pid not in self._seen: @@ -66,9 +65,9 @@ class ParticipantList(AsyncList[Participant]): self._offset += len(chanp.participants) self._done = len(self._seen) == seen_count - elif self._packed.is_chat(): + else: chatp = await self._client( - functions.messages.get_full_chat(chat_id=self._packed.id) + functions.messages.get_full_chat(chat_id=self._peer._to_input_chat()) ) assert isinstance(chatp, types.messages.ChatFull) assert isinstance(chatp.full_chat, types.ChatFull) @@ -81,48 +80,45 @@ class ParticipantList(AsyncList[Participant]): self._buffer.append( Participant._from_raw_chat( self._client, - self._packed, + self._peer, participants.self_participant, chat_map, ) ) elif isinstance(participants, types.ChatParticipants): self._buffer.extend( - Participant._from_raw_chat(self._client, self._packed, p, chat_map) + Participant._from_raw_chat(self._client, self._peer, 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) +def get_participants( + self: Client, chat: Group | Channel | GroupRef | ChannelRef, / +) -> AsyncList[Participant]: + return ParticipantList(self, chat._ref) class RecentActionList(AsyncList[RecentAction]): def __init__( self, client: Client, - chat: ChatLike, + peer: ChannelRef | GroupRef, ): super().__init__() self._client = client - self._chat = chat - self._peer: Optional[types.InputChannel] = None + self._peer = peer self._offset = 0 async def _fetch_next(self) -> None: - if self._peer is None: - self._peer = ( - await self._client._resolve_to_packed(self._chat) - )._to_input_channel() + if not isinstance(self._peer, ChannelRef): + return # small group chats have no recent actions result = await self._client( functions.channels.get_admin_log( - channel=self._peer, + channel=self._peer._to_input_channel(), q="", min_id=0, max_id=self._offset, @@ -141,34 +137,28 @@ class RecentActionList(AsyncList[RecentAction]): self._offset = min(e.id for e in self._buffer) -def get_admin_log(self: Client, chat: ChatLike) -> AsyncList[RecentAction]: - return RecentActionList(self, chat) +def get_admin_log( + self: Client, chat: Group | Channel | GroupRef | ChannelRef, / +) -> AsyncList[RecentAction]: + return RecentActionList(self, chat._ref) class ProfilePhotoList(AsyncList[File]): def __init__( self, client: Client, - chat: ChatLike, + peer: PeerRef, ): super().__init__() self._client = client - self._chat = chat - self._peer: Optional[abcs.InputPeer] = None + self._peer = peer self._search_iter: Optional[SearchList] = None async def _fetch_next(self) -> None: - if self._peer is None: - self._peer = ( - await self._client._resolve_to_packed(self._chat) - )._to_input_peer() - - if isinstance(self._peer, types.InputPeerUser): + if isinstance(self._peer, UserRef): result = await self._client( functions.photos.get_user_photos( - user_id=types.InputUser( - user_id=self._peer.user_id, access_hash=self._peer.access_hash - ), + user_id=self._peer._to_input_user(), offset=0, max_id=0, limit=0, @@ -191,86 +181,86 @@ class ProfilePhotoList(AsyncList[File]): ) -def get_profile_photos(self: Client, chat: ChatLike) -> AsyncList[File]: - return ProfilePhotoList(self, chat) +def get_profile_photos(self: Client, peer: Peer | PeerRef, /) -> AsyncList[File]: + return ProfilePhotoList(self, peer._ref) async def set_participant_admin_rights( - self: Client, chat: ChatLike, user: ChatLike, rights: Sequence[AdminRight] + self: Client, + chat: Group | Channel | GroupRef | ChannelRef, + /, + participant: User | UserRef, + rights: Sequence[AdminRight], ) -> None: - packed = await self._resolve_to_packed(chat) - participant = await self._resolve_to_packed(user) - - if packed.is_channel(): + chat = chat._ref + user = participant._ref + if isinstance(chat, ChannelRef): admin_rights = AdminRight._set_to_raw(set(rights)) await self( functions.channels.edit_admin( - channel=packed._to_input_channel(), - user_id=participant._to_input_user(), + channel=chat._to_input_channel(), + user_id=user._to_input_user(), admin_rights=admin_rights, rank="", ) ) - elif packed.is_chat(): + else: await self( functions.messages.edit_chat_admin( - chat_id=packed.id, - user_id=participant._to_input_user(), + chat_id=chat._to_input_chat(), + user_id=user._to_input_user(), is_admin=bool(rights), ) ) - else: - raise TypeError(f"Cannot set admin rights in {packed.ty}") async def set_participant_restrictions( self: Client, - chat: ChatLike, - user: ChatLike, + chat: Group | Channel | GroupRef | ChannelRef, + /, + participant: Peer | PeerRef, restrictions: Sequence[ChatRestriction], *, until: Optional[datetime.datetime] = None, ) -> None: - packed = await self._resolve_to_packed(chat) - participant = await self._resolve_to_packed(user) - if packed.is_channel(): + chat = chat._ref + peer = participant._ref + if isinstance(chat, ChannelRef): banned_rights = ChatRestriction._set_to_raw( set(restrictions), until_date=int(until.timestamp()) if until else 0x7FFFFFFF, ) await self( functions.channels.edit_banned( - channel=packed._to_input_channel(), - participant=participant._to_input_peer(), + channel=chat._to_input_channel(), + participant=peer._to_input_peer(), banned_rights=banned_rights, ) ) - elif packed.is_chat(): + elif isinstance(peer, UserRef): if restrictions: await self( functions.messages.delete_chat_user( revoke_history=ChatRestriction.VIEW_MESSAGES in restrictions, - chat_id=packed.id, - user_id=participant._to_input_user(), + chat_id=chat._to_input_chat(), + user_id=peer._to_input_user(), ) ) - else: - raise TypeError(f"Cannot set banned rights in {packed.ty}") async def set_chat_default_restrictions( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, restrictions: Sequence[ChatRestriction], *, until: Optional[datetime.datetime] = None, ) -> None: - peer = (await self._resolve_to_packed(chat))._to_input_peer() banned_rights = ChatRestriction._set_to_raw( set(restrictions), int(until.timestamp()) if until else 0x7FFFFFFF ) await self( functions.messages.edit_chat_default_banned_rights( - peer=peer, banned_rights=banned_rights + peer=chat._ref._to_input_peer(), banned_rights=banned_rights ) ) diff --git a/client/src/telethon/_impl/client/client/client.py b/client/src/telethon/_impl/client/client/client.py index a7d45db2..5a45f9e0 100644 --- a/client/src/telethon/_impl/client/client/client.py +++ b/client/src/telethon/_impl/client/client/client.py @@ -9,14 +9,17 @@ from typing import Any, Literal, Optional, Self, Sequence, Type, TypeVar from ....version import __version__ as default_version from ...mtsender import Connector, Sender from ...session import ( + ChannelRef, ChatHashCache, DataCenter, + GroupRef, MemorySession, MessageBox, PeerRef, Session, SqliteSession, Storage, + UserRef, ) from ...tl import Request, abcs from ..events import Event @@ -25,11 +28,12 @@ from ..types import ( AdminRight, AlbumBuilder, AsyncList, - ChatLike, + Channel, ChatRestriction, Dialog, Draft, File, + Group, InFileLike, InlineResult, LoginToken, @@ -103,14 +107,7 @@ from .updates import ( remove_event_handler, set_handler_filter, ) -from .users import ( - get_chats, - get_contacts, - get_me, - input_to_peer, - resolve_to_packed, - resolve_username, -) +from .users import get_contacts, get_me, resolve_peers, resolve_phone, resolve_username Return = TypeVar("Return") T = TypeVar("T") @@ -252,9 +249,9 @@ class Client: self._message_box = MessageBox(base_logger=base_logger) self._chat_hashes = ChatHashCache(None) self._last_update_limit_warn: Optional[float] = None - self._updates: asyncio.Queue[ - tuple[abcs.Update, dict[int, Peer]] - ] = asyncio.Queue(maxsize=self._config.update_queue_limit or 0) + self._updates: asyncio.Queue[tuple[abcs.Update, dict[int, Peer]]] = ( + asyncio.Queue(maxsize=self._config.update_queue_limit or 0) + ) self._dispatcher: Optional[asyncio.Task[None]] = None self._handlers: dict[ Type[Event], list[tuple[Callable[[Any], Awaitable[Any]], Optional[Filter]]] @@ -269,6 +266,7 @@ class Client: def add_event_handler( self, handler: Callable[[Event], Awaitable[Any]], + /, event_cls: Type[Event], filter: Optional[Filter] = None, ) -> None: @@ -387,7 +385,7 @@ class Client: """ await connect(self) - async def delete_dialog(self, chat: ChatLike) -> None: + async def delete_dialog(self, dialog: Peer | PeerRef, /) -> None: """ Delete a dialog. @@ -397,8 +395,8 @@ class Client: Note that bot accounts do not have dialogs, so this method will fail when used in a bot account. - :param chat: - The :term:`chat` representing the dialog to delete. + :param dialog: + The :term:`peer` representing the dialog to delete. .. rubric:: Example @@ -409,16 +407,16 @@ class Client: # You've realized you're more of a cat person await client.delete_dialog(dialog.chat) """ - await delete_dialog(self, chat) + await delete_dialog(self, dialog) async def delete_messages( - self, chat: ChatLike, message_ids: list[int], *, revoke: bool = True + self, chat: Peer | PeerRef, /, message_ids: list[int], *, revoke: bool = True ) -> int: """ Delete messages. :param chat: - The :term:`chat` where the messages are. + The :term:`peer` where the messages are. .. warning:: @@ -468,7 +466,7 @@ class Client: """ await disconnect(self) - async def download(self, media: File, file: str | Path | OutFileLike) -> None: + async def download(self, media: File, /, file: str | Path | OutFileLike) -> None: """ Download a file. @@ -504,7 +502,8 @@ class Client: async def edit_draft( self, - chat: ChatLike, + peer: Peer | PeerRef, + /, text: Optional[str] = None, *, markdown: Optional[str] = None, @@ -517,8 +516,8 @@ class Client: This can also be used to clear the draft by setting the text to an empty string ``""``. - :param chat: - The :term:`chat` where the draft will be saved to. + :param peer: + The :term:`peer` where the draft will be saved to. :param text: See :ref:`formatting`. :param markdown: See :ref:`formatting`. @@ -552,7 +551,7 @@ class Client: """ return await edit_draft( self, - chat, + peer, text, markdown=markdown, html=html, @@ -562,7 +561,8 @@ class Client: async def edit_message( self, - chat: ChatLike, + chat: Peer | PeerRef, + /, message_id: int, *, text: Optional[str] = None, @@ -575,7 +575,7 @@ class Client: Edit a message. :param chat: - The :term:`chat` where the message to edit is. + The :term:`peer` where the message to edit is. :param message_id: The identifier of the message to edit. @@ -617,19 +617,19 @@ class Client: ) async def forward_messages( - self, target: ChatLike, message_ids: list[int], source: ChatLike + self, target: Peer | PeerRef, message_ids: list[int], source: Peer | PeerRef ) -> list[Message]: """ - Forward messages from one :term:`chat` to another. + Forward messages from one :term:`peer` to another. :param target: - The :term:`chat` where the messages will be forwarded to. + The :term:`peer` where the messages will be forwarded to. :param message_ids: The list of message identifiers to forward. :param source: - The source :term:`chat` where the messages to forward exist. + The source :term:`peer` where the messages to forward exist. :return: The forwarded messages. @@ -651,16 +651,18 @@ class Client: """ return await forward_messages(self, target, message_ids, source) - def get_admin_log(self, chat: ChatLike) -> AsyncList[RecentAction]: + def get_admin_log( + self, chat: Group | Channel | GroupRef | ChannelRef, / + ) -> AsyncList[RecentAction]: """ Get the recent actions from the administrator's log. - This method requires you to be an administrator in the :term:`chat`. + This method requires you to be an administrator in the :term:`peer`. The returned actions are also known as "admin log events". :param chat: - The :term:`chat` to fetch recent actions from. + The :term:`peer` to fetch recent actions from. :return: The recent actions. @@ -674,42 +676,6 @@ class Client: """ return get_admin_log(self, chat) - async def get_chats( - self, chats: list[ChatLike] | tuple[ChatLike, ...] - ) -> list[Peer]: - """ - Get the latest basic information about the given chats. - - This method is most commonly used to turn one or more :class:`~types.PeerRef` into the original :class:`~types.Peer`. - This includes users, groups and broadcast channels. - - :param chats: - The users, groups or channels to fetch. - - :return: The fetched chats. - - .. rubric:: Example - - .. code-block:: python - - # Retrieve a PackedChat from somewhere - packed_user = my_database.get_packed_winner() - - # Fetch it - users = await client.get_chats([packed_user]) - user = users[0] # user will be a User if our packed_user was a user - - # Notify the user they won, using their current full name in the message - await client.send_message(packed_user, f'Congratulations {user.name}, you won!') - - .. caution:: - - This method supports being called with anything that looks like a chat, like every other method. - However, calling it with usernames or phone numbers will fetch the chats twice. - If that's the case, consider using :meth:`resolve_username` or :meth:`get_contacts` instead. - """ - return await get_chats(self, chats) - def get_contacts(self) -> AsyncList[User]: """ Get the users in your contact list. @@ -763,7 +729,7 @@ class Client: """ return get_drafts(self) - def get_file_bytes(self, media: File) -> AsyncList[bytes]: + def get_file_bytes(self, media: File, /) -> AsyncList[bytes]: """ Get the contents of an uploaded media file as chunks of :class:`bytes`. @@ -790,7 +756,7 @@ class Client: return get_file_bytes(self, media) def get_handler_filter( - self, handler: Callable[[Event], Awaitable[Any]] + self, handler: Callable[[Event], Awaitable[Any]], / ) -> Optional[Filter]: """ Get the filter associated to the given event handler. @@ -841,19 +807,20 @@ class Client: def get_messages( self, - chat: ChatLike, + chat: Peer | PeerRef, + /, limit: Optional[int] = None, *, offset_id: Optional[int] = None, offset_date: Optional[datetime.datetime] = None, ) -> AsyncList[Message]: """ - Get the message history from a :term:`chat`, from the newest message to the oldest. + Get the message history from a :term:`peer`, from the newest message to the oldest. The returned iterator can be :func:`reversed` to fetch from the first to the last instead. :param chat: - The :term:`chat` where the messages should be fetched from. + The :term:`peer` where the messages should be fetched from. :param limit: How many messages to fetch at most. @@ -891,13 +858,13 @@ class Client: ) def get_messages_with_ids( - self, chat: ChatLike, message_ids: list[int] + self, chat: Peer | PeerRef, /, message_ids: list[int] ) -> AsyncList[Message]: """ Get the full message objects from the corresponding message identifiers. :param chat: - The :term:`chat` where the message to fetch is. + The :term:`peer` where the message to fetch is. :param message_ids: The message identifiers of the messages to fetch. @@ -916,7 +883,9 @@ class Client: """ return get_messages_with_ids(self, chat, message_ids) - def get_participants(self, chat: ChatLike) -> AsyncList[Participant]: + def get_participants( + self, chat: Group | Channel | GroupRef | ChannelRef, / + ) -> AsyncList[Participant]: """ Get the participants in a group or channel, along with their permissions. @@ -927,7 +896,7 @@ class Client: There is no way to bypass this. :param chat: - The :term:`chat` to fetch participants from. + The :term:`peer` to fetch participants from. :return: The participants. @@ -940,12 +909,12 @@ class Client: """ return get_participants(self, chat) - def get_profile_photos(self, chat: ChatLike) -> AsyncList[File]: + def get_profile_photos(self, peer: Peer | PeerRef, /) -> AsyncList[File]: """ Get the profile pictures set in a chat, or user avatars. - :param chat: - The :term:`chat` to fetch the profile photo files from. + :param peer: + The :term:`peer` to fetch the profile photo files from. :return: The photo files. @@ -958,10 +927,15 @@ class Client: await client.download(photo, f'{i}.jpg') i += 1 """ - return get_profile_photos(self, chat) + return get_profile_photos(self, peer) async def inline_query( - self, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None + self, + bot: User | UserRef, + /, + query: str = "", + *, + peer: Optional[Peer | PeerRef] = None, ) -> AsyncIterator[InlineResult]: """ Perform a *@bot inline query*. @@ -975,7 +949,7 @@ class Client: :param query: The query string to send to the bot. - :param chat: + :param peer: Where the query is being made and will be sent. Some bots display different results based on the type of chat. @@ -997,7 +971,7 @@ class Client: if i == 10: break # did not find 'keyword' in the first few results """ - return await inline_query(self, bot, query, chat=chat) + return await inline_query(self, bot, query, peer=peer) async def interactive_login( self, phone_or_token: Optional[str] = None, *, password: Optional[str] = None @@ -1052,7 +1026,7 @@ class Client: return await is_authorized(self) def on( - self, event_cls: Type[Event], filter: Optional[Filter] = None + self, event_cls: Type[Event], /, filter: Optional[Filter] = None ) -> Callable[ [Callable[[Event], Awaitable[Any]]], Callable[[Event], Awaitable[Any]] ]: @@ -1093,12 +1067,12 @@ class Client: """ return on(self, event_cls, filter) - async def pin_message(self, chat: ChatLike, message_id: int) -> Message: + async def pin_message(self, chat: Peer | PeerRef, /, message_id: int) -> Message: """ Pin a message to be at the top. :param chat: - The :term:`chat` where the message to pin is. + The :term:`peer` where the message to pin is. :param message_id: The identifier of the message to pin. @@ -1144,7 +1118,7 @@ class Client: return prepare_album(self) async def read_message( - self, chat: ChatLike, message_id: int | Literal["all"] + self, chat: Peer | PeerRef, /, message_id: int | Literal["all"] ) -> None: """ Mark messages as read. @@ -1176,7 +1150,9 @@ class Client: """ await read_message(self, chat, message_id) - def remove_event_handler(self, handler: Callable[[Event], Awaitable[Any]]) -> None: + def remove_event_handler( + self, handler: Callable[[Event], Awaitable[Any]], / + ) -> None: """ Remove the handler as a function to be called when events occur. This is simply the opposite of :meth:`add_event_handler`. @@ -1227,16 +1203,60 @@ class Client: """ return await request_login_code(self, phone) - async def resolve_username(self, username: str) -> Peer: + async def resolve_peers(self, peers: Sequence[Peer | PeerRef], /) -> list[Peer]: """ - Resolve a username into a :term:`chat`. + Resolve one or more peer references into peer objects. + + This methods also accepts peer objects as input, which will be refetched but not mutated in-place. + + :param peers: + The peers to fetch. + + :return: The fetched peers, in the same order as the input. + + .. rubric:: Example + + .. code-block:: python + + [user, group, channel] = await client.resolve_peers([ + user_ref, group_ref, channel_ref + ]) + """ + return await resolve_peers(self, peers) + + async def resolve_phone(self, phone: str, /) -> Peer: + """ + Resolve a phone number into a :term:`peer`. This method is rather expensive to call. - It is recommended to use it once and then :meth:`types.Peer.pack` the result. - The packed chat can then be used (and re-fetched) more cheaply. + It is recommended to use it once and then store the :attr:`types.Peer.ref`. + + :param phone: + The phone number "+1 23 456" to resolve. + The phone number must contain the `International Calling Code `_. + You do not need to use include the ``'+'`` prefix, but the parameter must be a :class:`str`, not :class:`int`. + + :return: The matching chat. + + .. rubric:: Example + + .. code-block:: python + + print(await client.resolve_phone('+1 23 456')) + """ + return await resolve_phone(self, phone) + + async def resolve_username(self, username: str, /) -> Peer: + """ + Resolve a username into a :term:`peer`. + + This method is rather expensive to call. + It is recommended to use it once and then store the :attr:`types.Peer.ref`. :param username: The public "@username" to resolve. + You do not need to use include the ``'@'`` prefix. + Links cannot be used. :return: The matching chat. @@ -1268,9 +1288,6 @@ class Client: Perform a global message search. This is used to search messages in no particular chat (i.e. everywhere possible). - :param chat: - The :term:`chat` where the message to edit is. - :param limit: How many messages to fetch at most. @@ -1301,7 +1318,8 @@ class Client: def search_messages( self, - chat: ChatLike, + chat: Peer | PeerRef, + /, limit: Optional[int] = None, *, query: Optional[str] = None, @@ -1312,7 +1330,7 @@ class Client: Search messages in a chat. :param chat: - The :term:`chat` where messages will be searched. + The :term:`peer` where messages will be searched. :param limit: How many messages to fetch at most. @@ -1344,12 +1362,13 @@ class Client: async def send_audio( self, - chat: ChatLike, + chat: Peer | PeerRef, + /, file: str | Path | InFileLike | File, - mime_type: Optional[str] = None, *, size: Optional[int] = None, name: Optional[str] = None, + mime_type: Optional[str] = None, duration: Optional[float] = None, voice: bool = False, title: Optional[str] = None, @@ -1367,7 +1386,7 @@ class Client: duration, title and performer if they are not provided. :param chat: - The :term:`chat` where the audio media will be sent to. + The :term:`peer` where the audio media will be sent to. :param file: See :meth:`send_file`. :param size: See :meth:`send_file`. @@ -1391,9 +1410,9 @@ class Client: self, chat, file, - mime_type, size=size, name=name, + mime_type=mime_type, duration=duration, voice=voice, title=title, @@ -1407,7 +1426,8 @@ class Client: async def send_file( self, - chat: ChatLike, + chat: Peer | PeerRef, + /, file: str | Path | InFileLike | File, *, size: Optional[int] = None, @@ -1430,7 +1450,7 @@ class Client: caption_markdown: Optional[str] = None, caption_html: Optional[str] = None, reply_to: Optional[int] = None, - buttons: Optional[list[btns.Button] | list[list[btns.Button]]] = None, + buttons: Optional[list[btns.Button] | list[list[btns.Button]]], ) -> Message: """ Send any type of file with any amount of attributes. @@ -1442,7 +1462,7 @@ class Client: Unlike :meth:`send_photo`, image files will be sent as documents by default. :param chat: - The :term:`chat` where the message will be sent to. + The :term:`peer` where the message will be sent to. :param path: A local file path or :class:`~telethon.types.File` to send. @@ -1589,7 +1609,8 @@ class Client: async def send_message( self, - chat: ChatLike, + chat: Peer | PeerRef, + /, text: Optional[str | Message] = None, *, markdown: Optional[str] = None, @@ -1602,7 +1623,7 @@ class Client: Send a message. :param chat: - The :term:`chat` where the message will be sent to. + The :term:`peer` where the message will be sent to. :param text: See :ref:`formatting`. :param markdown: See :ref:`formatting`. @@ -1636,7 +1657,8 @@ class Client: async def send_photo( self, - chat: ChatLike, + chat: Peer | PeerRef, + /, file: str | Path | InFileLike | File, *, size: Optional[int] = None, @@ -1662,7 +1684,7 @@ class Client: width and height if they are not provided. :param chat: - The :term:`chat` where the photo media will be sent to. + The :term:`peer` where the photo media will be sent to. :param file: See :meth:`send_file`. :param size: See :meth:`send_file`. @@ -1700,7 +1722,8 @@ class Client: async def send_video( self, - chat: ChatLike, + chat: Peer | PeerRef, + /, file: str | Path | InFileLike | File, *, size: Optional[int] = None, @@ -1716,7 +1739,7 @@ class Client: caption_markdown: Optional[str] = None, caption_html: Optional[str] = None, reply_to: Optional[int] = None, - buttons: Optional[list[btns.Button] | list[list[btns.Button]]] = None, + buttons: Optional[list[btns.Button] | list[list[btns.Button]]], ) -> Message: """ Send a video file. @@ -1725,7 +1748,7 @@ class Client: duration, width and height if they are not provided. :param chat: - The :term:`chat` where the message will be sent to. + The :term:`peer` where the message will be sent to. :param file: See :meth:`send_file`. :param size: See :meth:`send_file`. @@ -1768,7 +1791,8 @@ class Client: async def set_chat_default_restrictions( self, - chat: ChatLike, + chat: Peer | PeerRef, + /, restrictions: Sequence[ChatRestriction], *, until: Optional[datetime.datetime] = None, @@ -1777,7 +1801,7 @@ class Client: Set the default restrictions to apply to all participant in a chat. :param chat: - The :term:`chat` where the restrictions will be applied. + The :term:`peer` where the restrictions will be applied. :param restrictions: The sequence of restrictions to apply. @@ -1810,6 +1834,7 @@ class Client: def set_handler_filter( self, handler: Callable[[Event], Awaitable[Any]], + /, filter: Optional[Filter] = None, ) -> None: """ @@ -1836,7 +1861,11 @@ class Client: set_handler_filter(self, handler, filter) async def set_participant_admin_rights( - self, chat: ChatLike, user: ChatLike, rights: Sequence[AdminRight] + self, + chat: Group | Channel | GroupRef | ChannelRef, + /, + participant: User | UserRef, + rights: Sequence[AdminRight], ) -> None: """ Set the administrator rights granted to the participant in the chat. @@ -1847,7 +1876,7 @@ class Client: In this case, granting any right will make the user an administrator with all rights. :param chat: - The :term:`chat` where the rights will be granted. + The :term:`peer` where the rights will be granted. :param participant: The participant to promote to administrator, usually a :class:`types.User`. @@ -1873,12 +1902,13 @@ class Client: :meth:`telethon.types.Participant.set_admin_rights` """ - await set_participant_admin_rights(self, chat, user, rights) + await set_participant_admin_rights(self, chat, participant, rights) async def set_participant_restrictions( self, - chat: ChatLike, - user: ChatLike, + chat: Group | Channel | GroupRef | ChannelRef, + /, + participant: Peer | PeerRef, restrictions: Sequence[ChatRestriction], *, until: Optional[datetime.datetime] = None, @@ -1893,7 +1923,7 @@ class Client: The participant's history will be revoked if the restriction to :attr:`~types.ChatRestriction.VIEW_MESSAGES` is applied. :param chat: - The :term:`chat` where the restrictions will be applied. + The :term:`peer` where the restrictions will be applied. :param participant: The participant to restrict or ban, usually a :class:`types.User`. @@ -1929,7 +1959,9 @@ class Client: :meth:`telethon.types.Participant.set_restrictions` """ - await set_participant_restrictions(self, chat, user, restrictions, until=until) + await set_participant_restrictions( + self, chat, participant, restrictions, until=until + ) async def sign_in(self, token: LoginToken, code: str) -> User | PasswordToken: """ @@ -1977,13 +2009,13 @@ class Client: await sign_out(self) async def unpin_message( - self, chat: ChatLike, message_id: int | Literal["all"] + self, chat: Peer | PeerRef, /, message_id: int | Literal["all"] ) -> None: """ Unpin one or all messages from the top. :param chat: - The :term:`chat` where the message pinned message is. + The :term:`peer` where the message pinned message is. :param message_id: The identifier of the message to unpin, or ``'all'`` to unpin them all. @@ -2014,16 +2046,10 @@ class Client: def _build_message_map( self, result: abcs.Updates, - peer: Optional[abcs.InputPeer], + peer: Optional[PeerRef], ) -> MessageMap: return build_message_map(self, result, peer) - async def _resolve_to_packed(self, chat: ChatLike) -> PeerRef: - return await resolve_to_packed(self, chat) - - def _input_to_peer(self, input: Optional[abcs.InputPeer]) -> Optional[abcs.Peer]: - return input_to_peer(self, input) - async def _upload( self, fd: str | Path | InFileLike, size: Optional[int], name: Optional[str] ) -> tuple[abcs.InputFile, str]: diff --git a/client/src/telethon/_impl/client/client/dialogs.py b/client/src/telethon/_impl/client/client/dialogs.py index d039c6d2..2b3cf9f3 100644 --- a/client/src/telethon/_impl/client/client/dialogs.py +++ b/client/src/telethon/_impl/client/client/dialogs.py @@ -3,12 +3,13 @@ from __future__ import annotations import time from typing import TYPE_CHECKING, Optional +from ...session import PeerRef from ...tl import functions, types from ..types import ( AsyncList, - ChatLike, Dialog, Draft, + Peer, build_chat_map, build_msg_map, parse_message, @@ -59,8 +60,8 @@ 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() +async def delete_dialog(self: Client, dialog: Peer | PeerRef, /) -> None: + peer = dialog._ref if isinstance(peer, types.InputPeerChannel): await self( functions.channels.leave_channel( @@ -119,7 +120,8 @@ def get_drafts(self: Client) -> AsyncList[Draft]: async def edit_draft( self: Client, - chat: ChatLike, + peer: Peer | PeerRef, + /, text: Optional[str] = None, *, markdown: Optional[str] = None, @@ -127,8 +129,7 @@ async def edit_draft( link_preview: bool = False, reply_to: Optional[int] = None, ) -> Draft: - packed = await self._resolve_to_packed(chat) - peer = (await self._resolve_to_packed(chat))._to_input_peer() + peer = peer._ref message, entities = parse_message( text=text, markdown=markdown, html=html, allow_empty=False ) @@ -138,7 +139,7 @@ async def edit_draft( no_webpage=not link_preview, reply_to_msg_id=reply_to, top_msg_id=None, - peer=peer, + peer=peer._to_input_peer(), message=message, entities=entities, ) @@ -147,7 +148,7 @@ async def edit_draft( return Draft._from_raw( client=self, - peer=packed._to_peer(), + peer=peer._to_peer(), top_msg_id=0, draft=types.DraftMessage( no_webpage=not link_preview, diff --git a/client/src/telethon/_impl/client/client/files.py b/client/src/telethon/_impl/client/client/files.py index c0ab0709..f7a91645 100644 --- a/client/src/telethon/_impl/client/client/files.py +++ b/client/src/telethon/_impl/client/client/files.py @@ -6,16 +6,17 @@ from inspect import isawaitable from pathlib import Path from typing import TYPE_CHECKING, Optional +from ...session import PeerRef from ...tl import abcs, functions, types from ..types import ( AlbumBuilder, AsyncList, - ChatLike, File, InFileLike, Message, OutFileLike, OutWrapper, + Peer, ) from ..types import buttons as btns from ..types import ( @@ -44,7 +45,8 @@ def prepare_album(self: Client) -> AlbumBuilder: async def send_photo( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, file: str | Path | InFileLike | File, *, size: Optional[int] = None, @@ -83,12 +85,13 @@ async def send_photo( async def send_audio( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, file: str | Path | InFileLike | File, - mime_type: Optional[str] = None, *, size: Optional[int] = None, name: Optional[str] = None, + mime_type: Optional[str] = None, duration: Optional[float] = None, voice: bool = False, title: Optional[str] = None, @@ -120,7 +123,8 @@ async def send_audio( async def send_video( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, file: str | Path | InFileLike | File, *, size: Optional[int] = None, @@ -161,7 +165,8 @@ async def send_video( async def send_file( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, file: str | Path | InFileLike | File, *, size: Optional[int] = None, @@ -289,14 +294,13 @@ async def send_file( async def do_send_file( client: Client, - chat: ChatLike, + chat: Peer | PeerRef, input_media: abcs.InputMedia, message: str, entities: Optional[list[abcs.MessageEntity]], reply_to: Optional[int], buttons: Optional[list[btns.Button] | list[list[btns.Button]]], ) -> Message: - peer = (await client._resolve_to_packed(chat))._to_input_peer() random_id = generate_random_id() return client._build_message_map( await client( @@ -306,7 +310,7 @@ async def do_send_file( clear_draft=False, noforwards=False, update_stickersets_order=False, - peer=peer, + peer=chat._ref._to_input_peer(), reply_to=( types.InputReplyToMessage(reply_to_msg_id=reply_to, top_msg_id=None) if reply_to @@ -321,7 +325,7 @@ async def do_send_file( send_as=None, ) ), - peer, + chat._ref, ).with_random_id(random_id) @@ -452,11 +456,13 @@ class FileBytesList(AsyncList[bytes]): self._done = len(result.bytes) < MAX_CHUNK_SIZE -def get_file_bytes(self: Client, media: File) -> AsyncList[bytes]: +def get_file_bytes(self: Client, media: File, /) -> AsyncList[bytes]: return FileBytesList(self, media) -async def download(self: Client, media: File, file: str | Path | OutFileLike) -> None: +async def download( + self: Client, media: File, /, file: str | Path | OutFileLike +) -> None: fd = OutWrapper(file) try: async for chunk in get_file_bytes(self, media): diff --git a/client/src/telethon/_impl/client/client/messages.py b/client/src/telethon/_impl/client/client/messages.py index d606374d..13d31985 100644 --- a/client/src/telethon/_impl/client/client/messages.py +++ b/client/src/telethon/_impl/client/client/messages.py @@ -4,9 +4,9 @@ import datetime import sys from typing import TYPE_CHECKING, Literal, Optional, Self -from ...session import PeerRef +from ...session import ChannelRef, PeerRef from ...tl import abcs, functions, types -from ..types import AsyncList, ChatLike, Message, Peer, build_chat_map +from ..types import AsyncList, Message, Peer, build_chat_map from ..types import buttons as btns from ..types import generate_random_id, parse_message, peer_id @@ -16,7 +16,8 @@ if TYPE_CHECKING: async def send_message( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, text: Optional[str | Message] = None, *, markdown: Optional[str] = None, @@ -25,8 +26,6 @@ async def send_message( reply_to: Optional[int] = None, buttons: Optional[list[btns.Button] | list[list[btns.Button]]] = None, ) -> Message: - packed = await self._resolve_to_packed(chat) - peer = packed._to_input_peer() random_id = generate_random_id() if isinstance(text, Message): @@ -38,7 +37,7 @@ async def send_message( clear_draft=False, noforwards=not text.can_forward, update_stickersets_order=False, - peer=peer, + peer=chat._ref._to_input_peer(), reply_to=( types.InputReplyToMessage( reply_to_msg_id=text.replied_message_id, top_msg_id=None @@ -64,7 +63,7 @@ async def send_message( clear_draft=False, noforwards=False, update_stickersets_order=False, - peer=peer, + peer=chat._ref._to_input_peer(), reply_to=( types.InputReplyToMessage(reply_to_msg_id=reply_to, top_msg_id=None) if reply_to @@ -90,7 +89,7 @@ async def send_message( if self._session.user else None ), - peer_id=packed._to_peer(), + peer_id=chat._ref._to_peer(), reply_to=( types.MessageReplyHeader( reply_to_scheduled=False, @@ -109,12 +108,13 @@ async def send_message( ttl_period=result.ttl_period, ) else: - return self._build_message_map(result, peer).with_random_id(random_id) + return self._build_message_map(result, chat._ref).with_random_id(random_id) async def edit_message( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, message_id: int, *, text: Optional[str] = None, @@ -123,7 +123,6 @@ async def edit_message( link_preview: bool = False, buttons: Optional[list[btns.Button] | list[list[btns.Button]]] = None, ) -> Message: - peer = (await self._resolve_to_packed(chat))._to_input_peer() message, entities = parse_message( text=text, markdown=markdown, html=html, allow_empty=False ) @@ -131,7 +130,7 @@ async def edit_message( await self( functions.messages.edit_message( no_webpage=not link_preview, - peer=peer, + peer=chat._ref._to_input_peer(), id=message_id, message=message, media=None, @@ -140,18 +139,23 @@ async def edit_message( schedule_date=None, ) ), - peer, + chat._ref, ).with_id(message_id) async def delete_messages( - self: Client, chat: ChatLike, message_ids: list[int], *, revoke: bool = True + self: Client, + chat: Peer | PeerRef, + /, + message_ids: list[int], + *, + revoke: bool = True, ) -> int: - packed_chat = await self._resolve_to_packed(chat) - if packed_chat.is_channel(): + peer = chat._ref + if isinstance(peer, ChannelRef): affected = await self( functions.channels.delete_messages( - channel=packed_chat._to_input_channel(), id=message_ids + channel=peer._to_input_channel(), id=message_ids ) ) else: @@ -163,10 +167,8 @@ async def delete_messages( async def forward_messages( - self: Client, target: ChatLike, message_ids: list[int], source: ChatLike + self: Client, target: Peer | PeerRef, message_ids: list[int], source: Peer | PeerRef ) -> list[Message]: - to_peer = (await self._resolve_to_packed(target))._to_input_peer() - from_peer = (await self._resolve_to_packed(source))._to_input_peer() random_ids = [generate_random_id() for _ in message_ids] map = self._build_message_map( await self( @@ -177,16 +179,16 @@ async def forward_messages( drop_author=False, drop_media_captions=False, noforwards=False, - from_peer=from_peer, + from_peer=source._ref._to_input_peer(), id=message_ids, random_id=random_ids, - to_peer=to_peer, + to_peer=target._ref._to_input_peer(), top_msg_id=None, schedule_date=None, send_as=None, ) ), - to_peer, + target._ref, ) return [map.with_random_id(id) for id in random_ids] @@ -239,7 +241,7 @@ class HistoryList(MessageList): def __init__( self, client: Client, - chat: ChatLike, + peer: PeerRef, limit: int, *, offset_id: int, @@ -247,8 +249,7 @@ class HistoryList(MessageList): ): super().__init__() self._client = client - self._chat = chat - self._peer: Optional[abcs.InputPeer] = None + self._peer = peer self._limit = limit self._offset_id = offset_id self._offset_date = offset_date @@ -256,15 +257,10 @@ class HistoryList(MessageList): self._done = limit <= 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() - limit = min(max(self._limit, 1), 100) result = await self._client( functions.messages.get_history( - peer=self._peer, + peer=self._peer._to_input_peer(), offset_id=self._offset_id, offset_date=self._offset_date, add_offset=-limit if self._reversed else 0, @@ -286,7 +282,7 @@ class HistoryList(MessageList): def __reversed__(self) -> Self: new = self.__class__( self._client, - self._chat, + self._peer, self._limit, offset_id=1 if self._offset_id == 0 else self._offset_id, offset_date=self._offset_date, @@ -298,7 +294,8 @@ class HistoryList(MessageList): def get_messages( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, limit: Optional[int] = None, *, offset_id: Optional[int] = None, @@ -306,7 +303,7 @@ def get_messages( ) -> AsyncList[Message]: return HistoryList( self, - chat, + chat._ref, sys.maxsize if limit is None else limit, offset_id=offset_id or 0, offset_date=int(offset_date.timestamp()) if offset_date is not None else 0, @@ -317,25 +314,22 @@ class CherryPickedList(MessageList): def __init__( self, client: Client, - chat: ChatLike, + peer: PeerRef, ids: list[int], ): super().__init__() self._client = client - self._chat = chat - self._packed: Optional[PeerRef] = None + self._peer = peer self._ids: list[abcs.InputMessage] = [types.InputMessageId(id=id) for id in ids] async def _fetch_next(self) -> None: if not self._ids: return - if self._packed is None: - self._packed = await self._client._resolve_to_packed(self._chat) - if self._packed.is_channel(): + if isinstance(self._peer, ChannelRef): result = await self._client( functions.channels.get_messages( - channel=self._packed._to_input_channel(), id=self._ids[:100] + channel=self._peer._to_input_channel(), id=self._ids[:100] ) ) else: @@ -349,17 +343,18 @@ class CherryPickedList(MessageList): def get_messages_with_ids( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, message_ids: list[int], ) -> AsyncList[Message]: - return CherryPickedList(self, chat, message_ids) + return CherryPickedList(self, chat._ref, message_ids) class SearchList(MessageList): def __init__( self, client: Client, - chat: ChatLike, + peer: PeerRef, limit: int, *, query: str, @@ -368,8 +363,7 @@ class SearchList(MessageList): ): super().__init__() self._client = client - self._chat = chat - self._peer: Optional[abcs.InputPeer] = None + self._peer = peer self._limit = limit self._query = query self._filter = types.InputMessagesFilterEmpty() @@ -377,14 +371,9 @@ class SearchList(MessageList): self._offset_date = offset_date async def _fetch_next(self) -> None: - if self._peer is None: - self._peer = ( - await self._client._resolve_to_packed(self._chat) - )._to_input_peer() - result = await self._client( functions.messages.search( - peer=self._peer, + peer=self._peer._to_input_peer(), q=self._query, from_id=None, top_msg_id=None, @@ -411,7 +400,8 @@ class SearchList(MessageList): def search_messages( self: Client, - chat: ChatLike, + chat: Peer | PeerRef, + /, limit: Optional[int] = None, *, query: Optional[str] = None, @@ -420,7 +410,7 @@ def search_messages( ) -> AsyncList[Message]: return SearchList( self, - chat, + chat._ref, sys.maxsize if limit is None else limit, query=query or "", offset_id=offset_id or 0, @@ -475,8 +465,7 @@ class GlobalSearchList(MessageList): 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() + self._offset_peer = chat._ref._to_input_peer() def search_all_messages( @@ -496,54 +485,62 @@ def search_all_messages( ) -async def pin_message(self: Client, chat: ChatLike, message_id: int) -> Message: - peer = (await self._resolve_to_packed(chat))._to_input_peer() +async def pin_message( + self: Client, chat: Peer | PeerRef, /, message_id: int +) -> Message: return self._build_message_map( await self( functions.messages.update_pinned_message( - silent=True, unpin=False, pm_oneside=False, peer=peer, id=message_id + silent=True, + unpin=False, + pm_oneside=False, + peer=chat._ref._to_input_peer(), + id=message_id, ) ), - peer, + chat._ref, ).get_single() async def unpin_message( - self: Client, chat: ChatLike, message_id: int | Literal["all"] + self: Client, chat: Peer | PeerRef, /, message_id: int | Literal["all"] ) -> None: - peer = (await self._resolve_to_packed(chat))._to_input_peer() if message_id == "all": await self( functions.messages.unpin_all_messages( - peer=peer, + peer=chat._ref._to_input_peer(), top_msg_id=None, ) ) else: await self( functions.messages.update_pinned_message( - silent=True, unpin=True, pm_oneside=False, peer=peer, id=message_id + silent=True, + unpin=True, + pm_oneside=False, + peer=chat._ref._to_input_peer(), + id=message_id, ) ) async def read_message( - self: Client, chat: ChatLike, message_id: int | Literal["all"] + self: Client, chat: Peer | PeerRef, /, message_id: int | Literal["all"] ) -> None: - packed = await self._resolve_to_packed(chat) if message_id == "all": message_id = 0 - if packed.is_channel(): + peer = chat._ref + if isinstance(peer, ChannelRef): await self( functions.channels.read_history( - channel=packed._to_input_channel(), max_id=message_id + channel=peer._to_input_channel(), max_id=message_id ) ) else: await self( functions.messages.read_history( - peer=packed._to_input_peer(), max_id=message_id + peer=peer._ref._to_input_peer(), max_id=message_id ) ) @@ -554,7 +551,7 @@ class MessageMap: def __init__( self, client: Client, - peer: Optional[abcs.InputPeer], + peer: Optional[PeerRef], random_id_to_id: dict[int, int], id_to_message: dict[int, Message], ) -> None: @@ -580,7 +577,9 @@ class MessageMap: def _empty(self, id: int = 0) -> Message: return Message._from_raw( self._client, - types.MessageEmpty(id=id, peer_id=self._client._input_to_peer(self._peer)), + types.MessageEmpty( + id=id, peer_id=self._peer._to_peer() if self._peer else None + ), {}, ) @@ -588,7 +587,7 @@ class MessageMap: def build_message_map( client: Client, result: abcs.Updates, - peer: Optional[abcs.InputPeer], + peer: Optional[PeerRef], ) -> MessageMap: if isinstance(result, (types.Updates, types.UpdatesCombined)): updates = result.updates diff --git a/client/src/telethon/_impl/client/client/net.py b/client/src/telethon/_impl/client/client/net.py index 75bb0f1b..bd22686f 100644 --- a/client/src/telethon/_impl/client/client/net.py +++ b/client/src/telethon/_impl/client/client/net.py @@ -196,9 +196,7 @@ async def connect(self: Client) -> None: self._session.user = SessionUser( id=me.id, dc=self._sender.dc_id, bot=me.bot, username=me.username ) - packed = me.pack() - assert packed is not None - self._chat_hashes.set_self_user(packed) + self._chat_hashes.set_self_user(me.id, me.bot) self._dispatcher = asyncio.create_task(dispatcher(self)) diff --git a/client/src/telethon/_impl/client/client/updates.py b/client/src/telethon/_impl/client/client/updates.py index 740f82f1..591280c9 100644 --- a/client/src/telethon/_impl/client/client/updates.py +++ b/client/src/telethon/_impl/client/client/updates.py @@ -20,7 +20,7 @@ UPDATE_LIMIT_EXCEEDED_LOG_COOLDOWN = 300 def on( - self: Client, event_cls: Type[Event], filter: Optional[Filter] = None + self: Client, event_cls: Type[Event], /, filter: Optional[Filter] = None ) -> Callable[[Callable[[Event], Awaitable[Any]]], Callable[[Event], Awaitable[Any]]]: def wrapper( handler: Callable[[Event], Awaitable[Any]] @@ -34,6 +34,7 @@ def on( def add_event_handler( self: Client, handler: Callable[[Event], Awaitable[Any]], + /, event_cls: Type[Event], filter: Optional[Filter] = None, ) -> None: @@ -41,7 +42,7 @@ def add_event_handler( def remove_event_handler( - self: Client, handler: Callable[[Event], Awaitable[Any]] + self: Client, handler: Callable[[Event], Awaitable[Any]], / ) -> None: for event_cls, handlers in tuple(self._handlers.items()): for i in reversed(range(len(handlers))): @@ -52,7 +53,7 @@ def remove_event_handler( def get_handler_filter( - self: Client, handler: Callable[[Event], Awaitable[Any]] + self: Client, handler: Callable[[Event], Awaitable[Any]], / ) -> Optional[Filter]: for handlers in self._handlers.values(): for h, f in handlers: @@ -64,6 +65,7 @@ def get_handler_filter( def set_handler_filter( self: Client, handler: Callable[[Event], Awaitable[Any]], + /, filter: Optional[Filter] = None, ) -> None: for handlers in self._handlers.values(): diff --git a/client/src/telethon/_impl/client/client/users.py b/client/src/telethon/_impl/client/client/users.py index c85067d3..346e8ee5 100644 --- a/client/src/telethon/_impl/client/client/users.py +++ b/client/src/telethon/_impl/client/client/users.py @@ -1,21 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Sequence from ...mtproto import RpcError -from ...session import PackedType, PeerRef +from ...session import GroupRef, PeerRef, UserRef from ...tl import abcs, functions, types -from ..types import ( - AsyncList, - Channel, - ChatLike, - Group, - Peer, - User, - build_chat_map, - expand_peer, - peer_id, -) +from ..types import AsyncList, Peer, User, build_chat_map, expand_peer, peer_id if TYPE_CHECKING: from .client import Client @@ -62,35 +52,33 @@ def resolved_peer_to_chat(client: Client, resolved: abcs.contacts.ResolvedPeer) raise ValueError("no matching chat found in response") -async def resolve_phone(client: Client, phone: str) -> Peer: +async def resolve_phone(self: Client, phone: str, /) -> Peer: return resolved_peer_to_chat( - client, await client(functions.contacts.resolve_phone(phone=phone)) + self, await self(functions.contacts.resolve_phone(phone=phone)) ) -async def resolve_username(self: Client, username: str) -> Peer: +async def resolve_username(self: Client, username: str, /) -> Peer: return resolved_peer_to_chat( self, await self(functions.contacts.resolve_username(username=username)) ) -async def get_chats( - self: Client, chats: list[ChatLike] | tuple[ChatLike, ...] -) -> list[Peer]: - packed_chats: list[PeerRef] = [] +async def resolve_peers(self: Client, peers: Sequence[Peer | PeerRef], /) -> list[Peer]: + refs: list[PeerRef] = [] input_users: list[types.InputUser] = [] input_chats: list[int] = [] input_channels: list[types.InputChannel] = [] - for chat in chats: - packed = await resolve_to_packed(self, chat) - packed_chats.append(packed) - if packed.is_user(): - input_users.append(packed._to_input_user()) - elif packed.is_chat(): - input_chats.append(packed.id) + for peer in peers: + peer = peer._ref + refs.append(peer) + if isinstance(peer, UserRef): + input_users.append(peer._to_input_user()) + elif isinstance(peer, GroupRef): + input_chats.append(peer._to_input_chat()) else: - input_channels.append(packed._to_input_channel()) + input_channels.append(peer._to_input_channel()) if input_users: ret_users = await self(functions.users.get_users(id=input_users)) @@ -101,152 +89,18 @@ async def get_chats( if input_chats: ret_chats = await self(functions.messages.get_chats(id=input_chats)) assert isinstance(ret_chats, types.messages.Chats) - groups = list(ret_chats.chats) + chats = list(ret_chats.chats) else: - groups = [] + chats = [] if input_channels: ret_chats = await self(functions.channels.get_channels(id=input_channels)) assert isinstance(ret_chats, types.messages.Chats) - channels = list(ret_chats.chats) - else: - channels = [] + chats.extend(ret_chats.chats) - chat_map = build_chat_map(self, users, groups + channels) + chat_map = build_chat_map(self, users, chats) return [ - chat_map.get(chat.id) - or expand_peer(self, chat._to_peer(), broadcast=chat.ty == PackedType.BROADCAST) - for chat in packed_chats + chat_map.get(ref.identifier) + or expand_peer(self, ref._to_peer(), broadcast=None) + for ref in refs ] - - -async def resolve_to_packed( - client: Client, chat: ChatLike | abcs.InputPeer | abcs.Peer -) -> PeerRef: - if isinstance(chat, PeerRef): - return chat - - if isinstance(chat, (User, Group, Channel)): - packed = chat.pack() or client._chat_hashes.get(chat.id) - if packed is not None: - return packed - - # Try anyway (may work for contacts or bot users). - if isinstance(chat, User): - ty = PackedType.USER - elif isinstance(chat, Group): - ty = PackedType.MEGAGROUP if chat.is_megagroup else PackedType.CHAT - else: - ty = PackedType.BROADCAST - - return PeerRef(ty=ty, id=chat.id, access_hash=0) - - if isinstance(chat, abcs.InputPeer): - if isinstance(chat, types.InputPeerEmpty): - raise ValueError("Cannot resolve chat") - elif isinstance(chat, types.InputPeerSelf): - if not client._session.user: - raise ValueError("Cannot resolve chat") - return PeerRef( - ty=PackedType.BOT if client._session.user.bot else PackedType.USER, - id=client._chat_hashes.self_id, - access_hash=0, - ) - elif isinstance(chat, types.InputPeerChat): - return PeerRef( - ty=PackedType.CHAT, - id=chat.chat_id, - access_hash=None, - ) - elif isinstance(chat, types.InputPeerUser): - return PeerRef( - ty=PackedType.USER, - id=chat.user_id, - access_hash=chat.access_hash, - ) - elif isinstance(chat, types.InputPeerChannel): - return PeerRef( - ty=PackedType.BROADCAST, - id=chat.channel_id, - access_hash=chat.access_hash, - ) - elif isinstance(chat, types.InputPeerUserFromMessage): - raise ValueError("Cannot resolve chat") - elif isinstance(chat, types.InputPeerChannelFromMessage): - raise ValueError("Cannot resolve chat") - else: - raise RuntimeError("unexpected case") - - if isinstance(chat, abcs.Peer): - packed = client._chat_hashes.get(peer_id(chat)) - if packed is not None: - return packed - if isinstance(chat, types.PeerUser): - return PeerRef( - ty=PackedType.USER, - id=chat.user_id, - access_hash=0, - ) - elif isinstance(chat, types.PeerChat): - return PeerRef( - ty=PackedType.CHAT, - id=chat.chat_id, - access_hash=0, - ) - elif isinstance(chat, types.PeerChannel): - return PeerRef( - ty=PackedType.BROADCAST, - id=chat.channel_id, - access_hash=0, - ) - else: - raise RuntimeError("unexpected case") - - if isinstance(chat, str): - if chat.startswith("+"): - resolved = await resolve_phone(client, chat) - elif chat == "me": - if me := client._session.user: - return PeerRef( - ty=PackedType.BOT if me.bot else PackedType.USER, - id=me.id, - access_hash=0, - ) - else: - resolved = None - else: - resolved = await resolve_username(client, username=chat) - - if resolved and (packed := resolved.pack()) is not None: - return packed - - if isinstance(chat, int): - packed = client._chat_hashes.get(chat) - if packed is None: - raise ValueError("Cannot resolve chat") - return packed - - raise ValueError("Cannot resolve chat") - - -def input_to_peer( - client: Client, input: Optional[abcs.InputPeer] -) -> Optional[abcs.Peer]: - if input is None: - return None - elif isinstance(input, types.InputPeerEmpty): - return None - elif isinstance(input, types.InputPeerSelf): - return types.PeerUser(user_id=client._chat_hashes.self_id) - elif isinstance(input, types.InputPeerChat): - return types.PeerChat(chat_id=input.chat_id) - elif isinstance(input, types.InputPeerUser): - return types.PeerUser(user_id=input.user_id) - elif isinstance(input, types.InputPeerChannel): - return types.PeerChannel(channel_id=input.channel_id) - elif isinstance(input, types.InputPeerUserFromMessage): - return None - elif isinstance(input, types.InputPeerChannelFromMessage): - return None - else: - raise RuntimeError("unexpected case") diff --git a/client/src/telethon/_impl/client/events/messages.py b/client/src/telethon/_impl/client/events/messages.py index 3a1d6cc1..65cf42de 100644 --- a/client/src/telethon/_impl/client/events/messages.py +++ b/client/src/telethon/_impl/client/events/messages.py @@ -149,7 +149,7 @@ class MessageRead(Event): @property def chat(self) -> Peer: """ - The :term:`chat` when the messages were read. + The :term:`peer` where the messages were read. """ peer = self._peer() pid = peer_id(peer) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index 44736bae..276c35c6 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -2,6 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Self +from ...session import PeerRef from ...tl import abcs, functions, types from ..client.messages import CherryPickedList from ..types import Message, Peer @@ -79,9 +80,9 @@ class ButtonCallback(Event): """ pid = peer_id(self._raw.peer) - chat = self._chat_map.get(pid) or await self._client._resolve_to_packed(pid) + chat = self._chat_map.get(pid) or PeerRef._empty_from_peer(self._raw.peer) - lst = CherryPickedList(self._client, chat, []) + lst = CherryPickedList(self._client, chat._ref, []) lst._ids.append( types.InputMessageCallbackQuery( id=self._raw.msg_id, query_id=self._raw.query_id diff --git a/client/src/telethon/_impl/client/types/__init__.py b/client/src/telethon/_impl/client/types/__init__.py index 5b2b9e9c..1082664a 100644 --- a/client/src/telethon/_impl/client/types/__init__.py +++ b/client/src/telethon/_impl/client/types/__init__.py @@ -25,16 +25,7 @@ from .message import ( from .meta import NoPublicConstructor from .participant import Participant from .password_token import PasswordToken -from .peer import ( - Channel, - ChatLike, - Group, - Peer, - User, - build_chat_map, - expand_peer, - peer_id, -) +from .peer import Channel, Group, Peer, User, build_chat_map, expand_peer, peer_id from .recent_action import RecentAction __all__ = [ @@ -45,7 +36,6 @@ __all__ = [ "CallbackAnswer", "Channel", "Peer", - "ChatLike", "Group", "User", "build_chat_map", diff --git a/client/src/telethon/_impl/client/types/album_builder.py b/client/src/telethon/_impl/client/types/album_builder.py index e8ce6603..5ead413f 100644 --- a/client/src/telethon/_impl/client/types/album_builder.py +++ b/client/src/telethon/_impl/client/types/album_builder.py @@ -4,11 +4,12 @@ import mimetypes from pathlib import Path from typing import TYPE_CHECKING, Optional +from ...session import PeerRef from ...tl import abcs, functions, types from .file import InFileLike, try_get_url_path from .message import Message, generate_random_id, parse_message from .meta import NoPublicConstructor -from .peer import ChatLike +from .peer import Peer if TYPE_CHECKING: from ..client.client import Client @@ -197,7 +198,7 @@ class AlbumBuilder(metaclass=NoPublicConstructor): ) async def send( - self, chat: ChatLike, *, reply_to: Optional[int] = None + self, peer: Peer | PeerRef, *, reply_to: Optional[int] = None ) -> list[Message]: """ Send the album. @@ -214,7 +215,6 @@ class AlbumBuilder(metaclass=NoPublicConstructor): messages = await album.send(chat) """ - peer = (await self._client._resolve_to_packed(chat))._to_input_peer() msg_map = self._client._build_message_map( await self._client( functions.messages.send_multi_media( @@ -223,7 +223,7 @@ class AlbumBuilder(metaclass=NoPublicConstructor): clear_draft=False, noforwards=False, update_stickersets_order=False, - peer=peer, + peer=peer._ref._to_input_peer(), reply_to=( types.InputReplyToMessage( reply_to_msg_id=reply_to, top_msg_id=None @@ -236,6 +236,6 @@ class AlbumBuilder(metaclass=NoPublicConstructor): send_as=None, ) ), - peer, + peer._ref, ) return [msg_map.with_random_id(media.random_id) for media in self._medias] diff --git a/client/src/telethon/_impl/client/types/buttons/callback.py b/client/src/telethon/_impl/client/types/buttons/callback.py index c43086fd..0bec0eac 100644 --- a/client/src/telethon/_impl/client/types/buttons/callback.py +++ b/client/src/telethon/_impl/client/types/buttons/callback.py @@ -47,14 +47,12 @@ class Callback(InlineButton): The bot's answer will be returned, or :data:`None` if they don't answer in time. """ message = self._message() - packed = message.chat.pack() - assert packed return CallbackAnswer._create( await message._client( functions.messages.get_bot_callback_answer( game=False, - peer=packed._to_input_peer(), + peer=message.chat._ref._to_input_peer(), msg_id=message.id, data=self.data, password=None, diff --git a/client/src/telethon/_impl/client/types/draft.py b/client/src/telethon/_impl/client/types/draft.py index 6de9d9b3..9368044d 100644 --- a/client/src/telethon/_impl/client/types/draft.py +++ b/client/src/telethon/_impl/client/types/draft.py @@ -150,7 +150,7 @@ class Draft(metaclass=NoPublicConstructor): new_draft = await old_draft.edit('new text', link_preview=False) """ return await self._client.edit_draft( - await self._packed_chat(), + self._peer_ref(), text, markdown=markdown, html=html, @@ -158,13 +158,11 @@ class Draft(metaclass=NoPublicConstructor): reply_to=reply_to, ) - async def _packed_chat(self) -> PeerRef: - packed = None + def _peer_ref(self) -> PeerRef: if chat := self._chat_map.get(peer_id(self._peer)): - packed = chat.pack() - if packed is None: - packed = await self._client._resolve_to_packed(peer_id(self._peer)) - return packed + return chat._ref + else: + return PeerRef._empty_from_peer(self._peer) async def send(self) -> Message: """ @@ -180,9 +178,6 @@ class Draft(metaclass=NoPublicConstructor): await draft.send(clear=False) """ - packed = await self._packed_chat() - peer = packed._to_input_peer() - reply_to = self.replied_message_id message = getattr(self._raw, "message", "") entities = getattr(self._raw, "entities", None) @@ -196,7 +191,7 @@ class Draft(metaclass=NoPublicConstructor): clear_draft=True, noforwards=False, update_stickersets_order=False, - peer=peer, + peer=self._peer_ref()._to_input_peer(), reply_to=( types.InputReplyToMessage(reply_to_msg_id=reply_to, top_msg_id=None) if reply_to @@ -221,7 +216,7 @@ class Draft(metaclass=NoPublicConstructor): if self._client._session.user else None ), - peer_id=packed._to_peer(), + peer_id=self._peer_ref()._to_peer(), reply_to=( types.MessageReplyHeader( reply_to_scheduled=False, @@ -240,9 +235,9 @@ class Draft(metaclass=NoPublicConstructor): ttl_period=result.ttl_period, ) else: - return self._client._build_message_map(result, peer).with_random_id( - random_id - ) + return self._client._build_message_map( + result, self._peer_ref() + ).with_random_id(random_id) async def delete(self) -> None: """ diff --git a/client/src/telethon/_impl/client/types/inline_result.py b/client/src/telethon/_impl/client/types/inline_result.py index 3dd7a234..86cfb34e 100644 --- a/client/src/telethon/_impl/client/types/inline_result.py +++ b/client/src/telethon/_impl/client/types/inline_result.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from ...tl import abcs, functions, types +from ...session import PeerRef +from ...tl import functions, types from .message import Message, generate_random_id from .meta import NoPublicConstructor -from .peer import ChatLike +from .peer import Peer if TYPE_CHECKING: from ..client.client import Client @@ -23,7 +24,7 @@ class InlineResult(metaclass=NoPublicConstructor): client: Client, results: types.messages.BotResults, result: types.BotInlineMediaResult | types.BotInlineResult, - default_peer: abcs.InputPeer, + default_peer: Optional[PeerRef], ): self._client = client self._raw_results = results @@ -50,7 +51,7 @@ class InlineResult(metaclass=NoPublicConstructor): async def send( self, - chat: Optional[ChatLike] = None, + peer: Optional[Peer | PeerRef] = None, ) -> Message: """ Send the result to the desired chat. @@ -62,13 +63,12 @@ class InlineResult(metaclass=NoPublicConstructor): :return: The sent message. """ - if chat is None and isinstance(self._default_peer, types.InputPeerEmpty): - raise ValueError("no target chat was specified") - - if chat is not None: - peer = (await self._client._resolve_to_packed(chat))._to_input_peer() - else: + if peer is None: + if self._default_peer is None: + raise ValueError("no target chat was specified") peer = self._default_peer + else: + peer = peer._ref random_id = generate_random_id() return self._client._build_message_map( @@ -78,7 +78,7 @@ class InlineResult(metaclass=NoPublicConstructor): background=False, clear_draft=False, hide_via=False, - peer=peer, + peer=peer._to_input_peer(), reply_to=None, random_id=random_id, query_id=self._raw_results.query_id, diff --git a/client/src/telethon/_impl/client/types/message.py b/client/src/telethon/_impl/client/types/message.py index 86b3749b..074d5dca 100644 --- a/client/src/telethon/_impl/client/types/message.py +++ b/client/src/telethon/_impl/client/types/message.py @@ -4,6 +4,7 @@ import datetime import time from typing import TYPE_CHECKING, Any, Optional, Self, Sequence +from ...session import PeerRef from ...tl import abcs, types from ..parsers import ( generate_html_message, @@ -14,7 +15,7 @@ from ..parsers import ( from .buttons import Button, as_concrete_row, create_button from .file import File from .meta import NoPublicConstructor -from .peer import ChatLike, Peer, expand_peer, peer_id +from .peer import Peer, expand_peer, peer_id if TYPE_CHECKING: from ..client.client import Client @@ -186,7 +187,7 @@ class Message(metaclass=NoPublicConstructor): @property def chat(self) -> Peer: """ - The :term:`chat` when the message was sent. + The :term:`peer` where the message was sent. """ peer = self._raw.peer_id or types.PeerUser(user_id=0) pid = peer_id(peer) @@ -199,7 +200,7 @@ class Message(metaclass=NoPublicConstructor): @property def sender(self) -> Optional[Peer]: """ - The :term:`chat` that sent the message. + The :term:`peer` that sent the message. This will usually be a :class:`User`, but can also be a :class:`Channel`. @@ -320,7 +321,7 @@ class Message(metaclass=NoPublicConstructor): if self.replied_message_id is not None: from ..client.messages import CherryPickedList - lst = CherryPickedList(self._client, self.chat, []) + lst = CherryPickedList(self._client, self.chat._ref, []) lst._ids.append(types.InputMessageReplyTo(id=self.id)) return (await lst)[0] return None @@ -415,7 +416,7 @@ class Message(metaclass=NoPublicConstructor): buttons=buttons, ) - async def forward(self, target: ChatLike) -> Message: + async def forward(self, target: Peer | PeerRef) -> Message: """ Alias for :meth:`telethon.Client.forward_messages`. diff --git a/client/src/telethon/_impl/client/types/participant.py b/client/src/telethon/_impl/client/types/participant.py index 864dbde2..ce7937b5 100644 --- a/client/src/telethon/_impl/client/types/participant.py +++ b/client/src/telethon/_impl/client/types/participant.py @@ -3,7 +3,7 @@ from __future__ import annotations import datetime from typing import TYPE_CHECKING, Optional, Self, Sequence -from ...session import PeerRef +from ...session import ChannelRef, GroupRef from ...tl import abcs, types from .admin_right import AdminRight from .chat_restriction import ChatRestriction @@ -24,7 +24,7 @@ class Participant(metaclass=NoPublicConstructor): def __init__( self, client: Client, - chat: PeerRef, + chat: GroupRef | ChannelRef, participant: ( types.ChannelParticipant | types.ChannelParticipantSelf @@ -47,7 +47,7 @@ class Participant(metaclass=NoPublicConstructor): def _from_raw_channel( cls, client: Client, - chat: PeerRef, + chat: ChannelRef, participant: abcs.ChannelParticipant, chat_map: dict[int, Peer], ) -> Self: @@ -70,7 +70,7 @@ class Participant(metaclass=NoPublicConstructor): def _from_raw_chat( cls, client: Client, - chat: PeerRef, + chat: GroupRef, participant: abcs.ChatParticipant, chat_map: dict[int, Peer], ) -> Self: @@ -193,7 +193,14 @@ class Participant(metaclass=NoPublicConstructor): """ participant = self.user or self.banned or self.left assert participant - await self._client.set_participant_admin_rights(self._chat, participant, rights) + if isinstance(participant, User): + await self._client.set_participant_admin_rights( + self._chat, participant, rights + ) + else: + raise TypeError( + f"participant of type {participant.__class__.__name__} cannot be made admin" + ) async def set_restrictions( self, diff --git a/client/src/telethon/_impl/client/types/peer/__init__.py b/client/src/telethon/_impl/client/types/peer/__init__.py index 9874f1ab..91c5d6ba 100644 --- a/client/src/telethon/_impl/client/types/peer/__init__.py +++ b/client/src/telethon/_impl/client/types/peer/__init__.py @@ -5,7 +5,6 @@ import sys from collections import defaultdict from typing import TYPE_CHECKING, Optional, Sequence -from ....session import PeerRef from ....tl import abcs, types from .channel import Channel from .group import Group @@ -15,8 +14,6 @@ from .user import User if TYPE_CHECKING: from ...client.client import Client -ChatLike = Peer | PeerRef | int | str - def build_chat_map( client: Client, users: Sequence[abcs.User], chats: Sequence[abcs.Chat] diff --git a/client/src/telethon/_impl/client/types/peer/channel.py b/client/src/telethon/_impl/client/types/peer/channel.py index 0f331db8..069b0040 100644 --- a/client/src/telethon/_impl/client/types/peer/channel.py +++ b/client/src/telethon/_impl/client/types/peer/channel.py @@ -1,6 +1,6 @@ from typing import Optional, Self -from ....session import PackedType, PeerRef +from ....session import ChannelRef from ....tl import abcs, types from ..meta import NoPublicConstructor from .peer import Peer @@ -50,18 +50,12 @@ class Channel(Peer, metaclass=NoPublicConstructor): def username(self) -> Optional[str]: return getattr(self._raw, "username", None) - def pack(self) -> Optional[PeerRef]: - if self._raw.access_hash is None: - return None - else: - return PeerRef( - ty=( - PackedType.GIGAGROUP - if getattr(self._raw, "gigagroup", False) - else PackedType.BROADCAST - ), - id=self._raw.id, - access_hash=self._raw.access_hash, - ) + @property + def ref(self) -> ChannelRef: + return ChannelRef(self._raw.id, self._raw.access_hash) + + @property + def _ref(self) -> ChannelRef: + return self.ref # endregion Overrides diff --git a/client/src/telethon/_impl/client/types/peer/group.py b/client/src/telethon/_impl/client/types/peer/group.py index 1dfcd148..5dce7ef3 100644 --- a/client/src/telethon/_impl/client/types/peer/group.py +++ b/client/src/telethon/_impl/client/types/peer/group.py @@ -3,7 +3,7 @@ from __future__ import annotations import datetime from typing import TYPE_CHECKING, Optional, Self, Sequence -from ....session import PackedType, PeerRef +from ....session import ChannelRef, GroupRef from ....tl import abcs, types from ..chat_restriction import ChatRestriction from ..meta import NoPublicConstructor @@ -65,17 +65,16 @@ class Group(Peer, metaclass=NoPublicConstructor): def username(self) -> Optional[str]: return getattr(self._raw, "username", None) - def pack(self) -> Optional[PeerRef]: + @property + def ref(self) -> GroupRef | ChannelRef: if isinstance(self._raw, (types.ChatEmpty, types.Chat, types.ChatForbidden)): - return PeerRef(ty=PackedType.CHAT, id=self._raw.id, access_hash=None) - elif self._raw.access_hash is None: - return None + return GroupRef(self._raw.id, None) else: - return PeerRef( - ty=PackedType.MEGAGROUP, - id=self._raw.id, - access_hash=self._raw.access_hash, - ) + return ChannelRef(self._raw.id, self._raw.access_hash) + + @property + def _ref(self) -> GroupRef | ChannelRef: + return self.ref # endregion Overrides diff --git a/client/src/telethon/_impl/client/types/peer/peer.py b/client/src/telethon/_impl/client/types/peer/peer.py index 94eb2984..ffbd7041 100644 --- a/client/src/telethon/_impl/client/types/peer/peer.py +++ b/client/src/telethon/_impl/client/types/peer/peer.py @@ -1,7 +1,7 @@ import abc from typing import Optional -from ....session import PeerRef +from ....session import ChannelRef, GroupRef, UserRef class Peer(abc.ABC): @@ -15,7 +15,7 @@ class Peer(abc.ABC): @abc.abstractmethod def id(self) -> int: """ - The chat's integer identifier. + The peer's integer identifier. This identifier is always a positive number. @@ -39,20 +39,28 @@ class Peer(abc.ABC): @abc.abstractmethod def username(self) -> Optional[str]: """ - The primary *@username* of the chat. + The primary *@username* of the user, group or chat. The returned string will *not* contain the at-sign ``@``. """ + @property @abc.abstractmethod - def pack(self) -> Optional[PeerRef]: + def ref(self) -> UserRef | GroupRef | ChannelRef: """ - Pack the chat into a compact and reusable object. + The reusable reference to this user, group or channel. - This object can be easily serialized and saved to persistent storage. - Unlike resolving usernames, packed chats can be reused without costly calls. + This can be used to persist the reference to a database or disk, + or to create inline mentions. .. seealso:: - :doc:`/concepts/chats` + :doc:`/concepts/peers` """ + + @property + def _ref(self) -> UserRef | GroupRef | ChannelRef: + """ + Private alias that also exists in refs to make conversion trivial. + """ + return self.ref diff --git a/client/src/telethon/_impl/client/types/peer/user.py b/client/src/telethon/_impl/client/types/peer/user.py index 6ac96afd..8ae92273 100644 --- a/client/src/telethon/_impl/client/types/peer/user.py +++ b/client/src/telethon/_impl/client/types/peer/user.py @@ -1,6 +1,6 @@ from typing import Optional, Self -from ....session import PackedType, PeerRef +from ....session import UserRef from ....tl import abcs, types from ..meta import NoPublicConstructor from .peer import Peer @@ -87,15 +87,13 @@ class User(Peer, metaclass=NoPublicConstructor): def username(self) -> Optional[str]: return self._raw.username - def pack(self) -> Optional[PeerRef]: - if self._raw.access_hash is None: - return None - else: - return PeerRef( - ty=PackedType.BOT if self._raw.bot else PackedType.USER, - id=self._raw.id, - access_hash=self._raw.access_hash, - ) + @property + def ref(self) -> UserRef: + return UserRef(self._raw.id, self._raw.access_hash) + + @property + def _ref(self) -> UserRef: + return self.ref # endregion Overrides diff --git a/client/src/telethon/_impl/session/__init__.py b/client/src/telethon/_impl/session/__init__.py index 848e6abb..12613aad 100644 --- a/client/src/telethon/_impl/session/__init__.py +++ b/client/src/telethon/_impl/session/__init__.py @@ -1,4 +1,12 @@ -from .chat import ChatHashCache, PackedType, PeerRef +from .chat import ( + ChannelRef, + ChatHashCache, + GroupRef, + PeerAuth, + PeerIdentifier, + PeerRef, + UserRef, +) from .message_box import ( BOT_CHANNEL_DIFF_LIMIT, NO_UPDATES_TIMEOUT, @@ -14,9 +22,13 @@ from .session import ChannelState, DataCenter, Session, UpdateState, User from .storage import MemorySession, SqliteSession, Storage __all__ = [ + "ChannelRef", "ChatHashCache", + "GroupRef", + "PeerAuth", + "PeerIdentifier", "PeerRef", - "PackedType", + "UserRef", "BOT_CHANNEL_DIFF_LIMIT", "NO_UPDATES_TIMEOUT", "USER_CHANNEL_DIFF_LIMIT", diff --git a/client/src/telethon/_impl/session/chat/__init__.py b/client/src/telethon/_impl/session/chat/__init__.py index 27f29d1c..82c31f86 100644 --- a/client/src/telethon/_impl/session/chat/__init__.py +++ b/client/src/telethon/_impl/session/chat/__init__.py @@ -1,4 +1,12 @@ from .hash_cache import ChatHashCache -from .peer_ref import PackedType, PeerRef +from .peer_ref import ChannelRef, GroupRef, PeerAuth, PeerIdentifier, PeerRef, UserRef -__all__ = ["ChatHashCache", "PeerRef", "PackedType"] +__all__ = [ + "ChatHashCache", + "ChannelRef", + "GroupRef", + "PeerAuth", + "PeerIdentifier", + "PeerRef", + "UserRef", +] diff --git a/client/src/telethon/_impl/session/chat/hash_cache.py b/client/src/telethon/_impl/session/chat/hash_cache.py index 6e0857f9..60117029 100644 --- a/client/src/telethon/_impl/session/chat/hash_cache.py +++ b/client/src/telethon/_impl/session/chat/hash_cache.py @@ -1,14 +1,16 @@ -from typing import Any, Optional, Sequence +from typing import Any, Optional, Sequence, Type, TypeAlias from ...tl import abcs, types -from .peer_ref import PackedType, PeerRef +from .peer_ref import ChannelRef, GroupRef, PeerRef, UserRef + +PeerRefType: TypeAlias = Type[UserRef] | Type[ChannelRef] | Type[GroupRef] class ChatHashCache: __slots__ = ("_hash_map", "_self_id", "_self_bot") def __init__(self, self_user: Optional[tuple[int, bool]]): - self._hash_map: dict[int, tuple[int, PackedType]] = {} + self._hash_map: dict[int, tuple[PeerRefType, int]] = {} self._self_id = self_user[0] if self_user else None self._self_bot = self_user[1] if self_user else False @@ -21,15 +23,14 @@ class ChatHashCache: def is_self_bot(self) -> bool: return self._self_bot - def set_self_user(self, user: PeerRef) -> None: - assert user.ty in (PackedType.USER, PackedType.BOT) - self._self_bot = user.ty == PackedType.BOT - self._self_id = user.id + def set_self_user(self, identifier: int, bot: bool) -> None: + self._self_id = identifier + self._self_bot = bot - def get(self, id: int) -> Optional[PeerRef]: - if (entry := self._hash_map.get(id)) is not None: - hash, ty = entry - return PeerRef(ty, id, hash) + def get(self, identifier: int) -> Optional[PeerRef]: + if (entry := self._hash_map.get(identifier)) is not None: + cls, authorization = entry + return cls(identifier, authorization) else: return None @@ -38,8 +39,8 @@ class ChatHashCache: self._self_id = None self._self_bot = False - def _has(self, id: int) -> bool: - return id in self._hash_map + def _has(self, identifier: int) -> bool: + return identifier in self._hash_map def _has_peer(self, peer: abcs.Peer) -> bool: if isinstance(peer, types.PeerUser): @@ -140,8 +141,7 @@ class ChatHashCache: pass elif isinstance(user, types.User): if not user.min and user.access_hash is not None: - ty = PackedType.BOT if user.bot else PackedType.USER - self._hash_map[user.id] = (user.access_hash, ty) + self._hash_map[user.id] = (UserRef, user.access_hash) else: success &= user.id in self._hash_map else: @@ -152,18 +152,11 @@ class ChatHashCache: pass elif isinstance(chat, types.Channel): if not chat.min and chat.access_hash is not None: - if chat.megagroup: - ty = PackedType.MEGAGROUP - elif chat.gigagroup: - ty = PackedType.GIGAGROUP - else: - ty = PackedType.BROADCAST - self._hash_map[chat.id] = (chat.access_hash, ty) + self._hash_map[chat.id] = (ChannelRef, chat.access_hash) else: success &= chat.id in self._hash_map elif isinstance(chat, types.ChannelForbidden): - ty = PackedType.MEGAGROUP if chat.megagroup else PackedType.BROADCAST - self._hash_map[chat.id] = (chat.access_hash, ty) + self._hash_map[chat.id] = (ChannelRef, chat.access_hash) else: raise RuntimeError("unexpected case") diff --git a/client/src/telethon/_impl/session/chat/peer_ref.py b/client/src/telethon/_impl/session/chat/peer_ref.py index b05c998f..25122d38 100644 --- a/client/src/telethon/_impl/session/chat/peer_ref.py +++ b/client/src/telethon/_impl/session/chat/peer_ref.py @@ -1,150 +1,254 @@ +from __future__ import annotations + +import abc +import base64 +import re import struct -from enum import IntFlag -from typing import Optional, Self +from typing import Optional, Self, TypeAlias from ...tl import abcs, types +PeerIdentifier: TypeAlias = int +PeerAuth: TypeAlias = Optional[int] -class PackedType(IntFlag): - """ - The type of a :class:`PeerRef`. +USER_PREFIX = "u." +GROUP_PREFIX = "g." +CHANNEL_PREFIX = "c." + + +class PeerRef(abc.ABC): """ + A reference to a :term:`peer`. - # bits: zero, has-access-hash, channel, broadcast, group, chat, user, bot - USER = 0b0000_0010 - BOT = 0b0000_0011 - CHAT = 0b0000_0100 - MEGAGROUP = 0b0010_1000 - BROADCAST = 0b0011_0000 - GIGAGROUP = 0b0011_1000 + References can be used to interact with any method that expects a peer, + without the need to fetch or resolve the entire peer beforehand. + A reference consists of both an identifier and the authorization to access the peer. + The proof of authorization is represented by Telegram's access hash witness. -class PeerRef: - """ - A compact representation of a :term:`chat`. - - You can reuse it as many times as you want. - - You can call ``chat.pack()`` on :class:`~telethon.types.User`, + You can access the :attr:`telethon.types.Peer.ref` attribute on :class:`~telethon.types.User`, :class:`~telethon.types.Group` or :class:`~telethon.types.Channel` to obtain it. + Not all references are always valid in all contexts. + Under certain conditions, it is possible for a reference without an authorization to be usable, + and for a reference with an authorization to not be usable everywhere. + The exact rules are defined by Telegram and could change any time. + .. seealso:: - :doc:`/concepts/chats` + :doc:`/concepts/peers` """ - __slots__ = ("ty", "id", "access_hash") + __slots__ = ("identifier", "authorization") - def __init__(self, ty: PackedType, id: int, access_hash: Optional[int]) -> None: - self.ty = ty - self.id = id - self.access_hash = access_hash - - def __bytes__(self) -> bytes: - return struct.pack( - " None: + assert ( + identifier >= 0 + ), "PeerRef identifiers must be positive; see the documentation for Peers" + self.identifier = identifier + self.authorization = authorization @classmethod - def from_bytes(cls, data: bytes) -> Self: - ty_byte, id, access_hash = struct.unpack(" str: + def from_str(cls, string: str, /) -> UserRef | GroupRef | ChannelRef: """ - Convenience property to convert to bytes and represent them as hexadecimal numbers: + Create a reference back from its string representation: - .. code-block:: + :param string: + The :class:`str` representation of the :class:`PeerRef`. - assert packed.hex == bytes(packed).hex() + .. rubric:: Example + + .. code-block:: python + + ref: PeerRef = ... + assert PeerRef.from_str(str(ref)) == ref """ - return bytes(self).hex() + if match := re.match(r"(\w\.)(\d+)\.([^.]+)", string): + prefix, iden, auth = match.groups() + + identifier = int(iden) + + if auth == "0": + authorization: Optional[int] = None + else: + try: + (authorization,) = struct.unpack( + "!q", base64.urlsafe_b64decode(auth.encode("ascii") + b"=") + ) + except Exception: + raise ValueError(f"invalid PeerRef string: {string!r}") + + if prefix == USER_PREFIX: + return UserRef(identifier, authorization) + elif prefix == GROUP_PREFIX: + return GroupRef(identifier, authorization) + elif prefix == CHANNEL_PREFIX: + return ChannelRef(identifier, authorization) + + raise ValueError(f"invalid PeerRef string: {string!r}") @classmethod - def from_hex(cls, hex: str) -> Self: - """ - Convenience method to convert hexadecimal numbers into bytes then passed to :meth:`from_bytes`: - - :param hex: - Hexadecimal numbers to convert from. - - .. code-block:: - - assert PackedChat.from_hex(packed.hex) == packed - """ - return cls.from_bytes(bytes.fromhex(hex)) - - def is_user(self) -> bool: - return self.ty in (PackedType.USER, PackedType.BOT) - - def is_chat(self) -> bool: - return self.ty in (PackedType.CHAT,) - - def is_channel(self) -> bool: - return self.ty in ( - PackedType.MEGAGROUP, - PackedType.BROADCAST, - PackedType.GIGAGROUP, - ) + def _empty_from_peer(cls, peer: abcs.Peer) -> UserRef | GroupRef | ChannelRef: + if isinstance(peer, types.PeerUser): + return UserRef(peer.user_id, None) + elif isinstance(peer, types.PeerChat): + return GroupRef(peer.chat_id, None) + elif isinstance(peer, types.PeerChannel): + return ChannelRef(peer.channel_id, None) + else: + raise RuntimeError("unexpected case") + @abc.abstractmethod def _to_peer(self) -> abcs.Peer: - if self.is_user(): - return types.PeerUser(user_id=self.id) - elif self.is_chat(): - return types.PeerChat(chat_id=self.id) - elif self.is_channel(): - return types.PeerChannel(channel_id=self.id) - else: - raise RuntimeError("unexpected case") + pass + @abc.abstractmethod def _to_input_peer(self) -> abcs.InputPeer: - if self.is_user(): - return types.InputPeerUser( - user_id=self.id, access_hash=self.access_hash or 0 - ) - elif self.is_chat(): - return types.InputPeerChat(chat_id=self.id) - elif self.is_channel(): - return types.InputPeerChannel( - channel_id=self.id, access_hash=self.access_hash or 0 - ) - else: - raise RuntimeError("unexpected case") + pass - def _to_input_user(self) -> types.InputUser: - if self.is_user(): - return types.InputUser(user_id=self.id, access_hash=self.access_hash or 0) - else: - raise TypeError("chat is not a user") + @abc.abstractmethod + def __str__(self) -> str: + """ + Format the reference into a :class:`str`. - def _to_chat_id(self) -> int: - if self.is_chat(): - return self.id - else: - raise TypeError("chat is not a group") + .. seealso:: - def _to_input_channel(self) -> types.InputChannel: - if self.is_channel(): - return types.InputChannel( - channel_id=self.id, access_hash=self.access_hash or 0 + :doc:`/concepts/messages`, to learn how this can be used to format inline mentions in messages. + """ + + def __repr__(self) -> str: + """ + Format the reference in a way that's easy to debug. + """ + return f"{self.__class__.__name__}({self.identifier}, {self.authorization})" + + def _encode_str(self) -> str: + if self.authorization is None: + auth = "0" + else: + auth = ( + base64.urlsafe_b64encode(struct.pack("!q", self.authorization)) + .decode("ascii") + .rstrip("=") ) - else: - raise TypeError("chat is not a channel") + + return f"{self.identifier}.{auth}" def __eq__(self, other: object) -> bool: if not isinstance(other, self.__class__): return NotImplemented return ( - self.ty == other.ty - and self.id == other.id - and self.access_hash == other.access_hash + self.identifier == other.identifier + and self.authorization == other.authorization + ) + + @property + def _ref(self) -> UserRef | GroupRef | ChannelRef: + assert isinstance(self, (UserRef, GroupRef, ChannelRef)) + return self + + +class UserRef(PeerRef): + """ + A user reference. + + This includes both user accounts and bot accounts, and corresponds to a bare Telegram :tl:`user`. + """ + + @classmethod + def from_str(cls, string: str, /) -> Self: + ref = super().from_str(string) + if not isinstance(ref, cls): + raise TypeError("PeerRef string does not belong to UserRef") + + return ref + + def _to_peer(self) -> abcs.Peer: + return types.PeerUser(user_id=self.identifier) + + def _to_input_peer(self) -> abcs.InputPeer: + return types.InputPeerUser( + user_id=self.identifier, access_hash=self.authorization or 0 + ) + + def _to_input_user(self) -> types.InputUser: + return types.InputUser( + user_id=self.identifier, access_hash=self.authorization or 0 ) def __str__(self) -> str: - return f"PackedChat.{self.ty.name}({self.id})" + return f"{USER_PREFIX}{self._encode_str()}" + + @property + def _ref(self) -> Self: + return self + + +class GroupRef(PeerRef): + """ + A group reference. + + This only includes small group chats, and corresponds to a bare Telegram :tl:`chat`. + """ + + @classmethod + def from_str(cls, string: str, /) -> Self: + ref = super().from_str(string) + if not isinstance(ref, cls): + raise TypeError("PeerRef string does not belong to GroupRef") + + return ref + + def _to_peer(self) -> abcs.Peer: + return types.PeerChat(chat_id=self.identifier) + + def _to_input_peer(self) -> abcs.InputPeer: + return types.InputPeerChat(chat_id=self.identifier) + + def _to_input_chat(self) -> int: + return self.identifier + + def __str__(self) -> str: + return f"{GROUP_PREFIX}{self._encode_str()}" + + @property + def _ref(self) -> Self: + return self + + +class ChannelRef(PeerRef): + """ + A channel reference. + + This includes broadcast channels, megagroups and gigagroups, and corresponds to a bare Telegram :tl:`channel`. + """ + + @classmethod + def from_str(cls, string: str, /) -> Self: + ref = super().from_str(string) + if not isinstance(ref, cls): + raise TypeError("PeerRef string does not belong to ChannelRef") + + return ref + + def _to_peer(self) -> abcs.Peer: + return types.PeerChannel(channel_id=self.identifier) + + def _to_input_peer(self) -> abcs.InputPeer: + return types.InputPeerChannel( + channel_id=self.identifier, access_hash=self.authorization or 0 + ) + + def _to_input_channel(self) -> types.InputChannel: + return types.InputChannel( + channel_id=self.identifier, access_hash=self.authorization or 0 + ) + + def __str__(self) -> str: + return f"{CHANNEL_PREFIX}{self._encode_str()}" + + @property + def _ref(self) -> Self: + return self diff --git a/client/src/telethon/_impl/session/message_box/messagebox.py b/client/src/telethon/_impl/session/message_box/messagebox.py index 738be714..4205bf78 100644 --- a/client/src/telethon/_impl/session/message_box/messagebox.py +++ b/client/src/telethon/_impl/session/message_box/messagebox.py @@ -546,12 +546,12 @@ class MessageBox: else: return None - packed = chat_hashes.get(id) - if packed: - assert packed.access_hash is not None + ref = chat_hashes.get(id) + if ref: + assert ref.authorization is not None channel = types.InputChannel( - channel_id=packed.id, - access_hash=packed.access_hash, + channel_id=ref.identifier, + access_hash=ref.authorization, ) if state := self.map.get(entry): gd = functions.updates.get_channel_difference( diff --git a/client/src/telethon/types/__init__.py b/client/src/telethon/types/__init__.py index 8acac75c..01c3c013 100644 --- a/client/src/telethon/types/__init__.py +++ b/client/src/telethon/types/__init__.py @@ -23,7 +23,7 @@ from .._impl.client.types import ( User, ) from .._impl.client.types.buttons import Button, InlineButton -from .._impl.session import PackedType, PeerRef +from .._impl.session import ChannelRef, GroupRef, PeerRef, UserRef __all__ = [ "AdminRight", @@ -46,6 +46,8 @@ __all__ = [ "User", "Button", "InlineButton", + "ChannelRef", + "GroupRef", "PeerRef", - "PackedType", + "UserRef", ] diff --git a/client/tests/packed_chat_test.py b/client/tests/packed_chat_test.py deleted file mode 100644 index f432a6b5..00000000 --- a/client/tests/packed_chat_test.py +++ /dev/null @@ -1,10 +0,0 @@ -from telethon._impl.session import PackedType, PeerRef - - -def test_hash_optional() -> None: - for ty in PackedType: - pc = PeerRef(ty, 123, 456789) - assert PeerRef.from_bytes(bytes(pc)) == pc - - pc = PeerRef(ty, 987, None) - assert PeerRef.from_bytes(bytes(pc)) == pc diff --git a/client/tests/peer_ref_test.py b/client/tests/peer_ref_test.py new file mode 100644 index 00000000..6a23246c --- /dev/null +++ b/client/tests/peer_ref_test.py @@ -0,0 +1,40 @@ +import inspect + +from pytest import raises +from telethon._impl.session import ChannelRef, GroupRef, PeerRef, UserRef + +USER = UserRef(12, 34) +GROUP = GroupRef(5, None) +CHANNEL = ChannelRef(67, 89) + + +def test_peer_ref() -> None: + assert PeerRef.from_str(str(USER)) == USER + assert PeerRef.from_str(str(GROUP)) == GROUP + assert PeerRef.from_str(str(CHANNEL)) == CHANNEL + + assert inspect.isabstract(PeerRef) + + with raises(ValueError): + PeerRef.from_str("invalid") + + +def test_user_ref() -> None: + assert UserRef.from_str(str(USER)) == USER + + with raises(TypeError): + UserRef.from_str(str(GROUP)) + + +def test_group_ref() -> None: + assert GroupRef.from_str(str(GROUP)) == GROUP + + with raises(TypeError): + GroupRef.from_str(str(CHANNEL)) + + +def test_channel_ref() -> None: + assert ChannelRef.from_str(str(CHANNEL)) == CHANNEL + + with raises(TypeError): + ChannelRef.from_str(str(USER)) diff --git a/tools/copy_client_signatures.py b/tools/copy_client_signatures.py index 907db7ed..d3e48520 100644 --- a/tools/copy_client_signatures.py +++ b/tools/copy_client_signatures.py @@ -28,7 +28,7 @@ class FunctionMethodsVisitor(ast.NodeVisitor): self._try_add_def(node) def _try_add_def(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: - match node.args.args: + match node.args.posonlyargs + node.args.args: case [ast.arg(arg="self", annotation=ast.Name(id="Client")), *_]: self.methods.append(node) case _: @@ -94,14 +94,20 @@ def main() -> None: call: ast.AST = ast.Call( func=ast.Name(id=function.name, ctx=ast.Load()), - args=[ast.Name(id=a.arg, ctx=ast.Load()) for a in function.args.args], + args=[ + ast.Name(id=a.arg, ctx=ast.Load()) + for a in function.args.posonlyargs + function.args.args + ], keywords=[ ast.keyword(arg=a.arg, value=ast.Name(id=a.arg, ctx=ast.Load())) for a in function.args.kwonlyargs ], ) - function.args.args[0].annotation = None + if function.args.posonlyargs: + function.args.posonlyargs[0].annotation = None + else: + function.args.args[0].annotation = None if isinstance(function, ast.AsyncFunctionDef): call = ast.Await(value=call)