Migrate from chat to peer

This commit is contained in:
Lonami Exo 2024-03-18 18:55:23 +01:00
parent 1dba3ae6d0
commit 38241dffd2
39 changed files with 841 additions and 802 deletions

View File

@ -6,10 +6,10 @@ Glossary
.. glossary:: .. glossary::
:sorted: :sorted:
chat peer
A :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`. A :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`.
.. seealso:: The :doc:`../concepts/chats` concept. .. seealso:: The :doc:`../concepts/peers` concept.
yourself yourself
The logged-in account, whether that represents a bot or a user with a phone number. The logged-in account, whether that represents a bot or a user with a phone number.

View File

@ -95,9 +95,9 @@ and instead favours more standard `HTML elements <https://developer.mozilla.org/
Both markdown and HTML recognise the following special URLs using the ``tg:`` protocol: Both markdown and HTML recognise the following special URLs using the ``tg:`` protocol:
* ``tg://user?id=ab1234cd6789`` for inline mentions. * ``tg://user?ref=u.123.A4B5`` for inline mentions.
To make sure the mention works, use :attr:`types.PeerRef.hex`. You can obtain the reference using :attr:`types.Peer.ref` (as in ``f'tg://user?ref={user.ref}'``).
You can also use :attr:`types.User.id`, but the mention will fail if the user is not in cache. You can also the ``?id=`` query parameter with :attr:`types.User.id` instead, but the mention may fail.
* ``tg://emoji?id=1234567890`` for custom emoji. * ``tg://emoji?id=1234567890`` for custom emoji.
You must use the document identifier as the value. You must use the document identifier as the value.
The alt-text of the image **must** be a emoji such as 👍. The alt-text of the image **must** be a emoji such as 👍.

View File

@ -1,43 +1,47 @@
Chats Peers, users and chats
===== ======================
.. currentmodule:: telethon .. currentmodule:: telethon
The term :term:`chat` is extremely overloaded, so it's no surprise many are confused by what it means. The term :term:`peer` may sound strange at first, but it's the best we have after much consideration.
This section should hopefully clear that up. This section aims to explain what peers are, and how they relate to users, group chats, and broadcast channels.
Telethon Chat Telethon Peer
------------- -------------
The word :term:`chat` in Telethon is used to refer a place where messages are sent to. The :class:`~types.Peer` type in Telethon is the base class for :class:`~types.User`, :class:`~types.Group` and :class:`~types.Channel`.
Therefore, a Telethon :term:`chat` can be another user, a bot, a group, or a broadcast channel. Therefore, a Telethon ":term:`peer`" represents an entity with various attributes: identifier, username, photo, title, and other information depending on its type.
All of those are places where messages can be sent.
Of course, chats do more things than contain messages. The :class:`~types.PeerRef` type represents a reference to a :class:`~types.Peer`, and can be obtained from its :attr:`~types.Peer.ref` attribute.
They often have a name, username, photo, description, and other information. Each peer type has its own reference type, namely :class:`~types.UserRef`, :class:`~types.GroupRef` and :class:`~types.ChannelRef`.
When a :term:`chat` appears in a parameter or as a property, | Most methods accept either the :class:`~types.Peer` or :class:`~types.PeerRef` (and their subclasses) as input.
it means that it will be either a :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`. You do not need to fetch the full :class:`~types.Peer` to :meth:`~Client.get_messages` or :meth:`~Client.send_file`\ s— a :class:`~types.PeerRef` is enough.
| Some methods will only work on groups and channels (like :meth:`~Client.get_participants`), or users (like :meth:`~Client.inline_query`).
When a parameter must be "chat-like", it means Telethon will accept anything that can be "converted" to a :term:`chat`. A Telethon "chat" refers to either groups and channels, or the place where messages are sent to.
The following types are chat-like: In the latter case, the chat could also belong to a user, so it would be represented by a :class:`~types.Peer`.
* The ``'me'`` literal string. This represents the account that is logged in ("yourself"). A Telethon "group" is used to refer to either small group chats or supergroups.
* An ``'@username'``. The at-sign ``@`` is optional. Note that links are not supported. This matches what the interface of official applications call these entities.
* An ``'+1 23'`` phone number string. It must be an :class:`str` and start with the plus-sign ``+`` character.
* An ``123`` integer identifier. It must be an :class:`int` and cannot be negative.
* An existing :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`.
* A :class:`~types.PeerRef`.
Previous versions of Telethon referred to this term as "entity" or "entities" instead. A Telethon "user" is used to refer to either user accounts or bot accounts.
This matches Telegram's API, as both are represented by the same user object.
Telegram Chat Telegram Peer
------------- -------------
The Telegram API is very confusing when it comes to the word "chat". .. note::
You only need to know about this if you plan to use the :term:`Raw API`.
This section is mainly of interest if you plan to use the :term:`Raw API`.
Telegram uses :tl:`Peer`\ s to categorize users, groups and channels, much like how Telethon does.
It also has the concept of :tl:`InputPeer`\ s, which are commonly used as input parameters when sending requests.
These match the concept of Telethon's peer references.
The main confusion in Telegram's API comes from the word "chat".
In the :term:`TL` schema definitions, there are two boxed types, :tl:`User` and :tl:`Chat`. In the :term:`TL` schema definitions, there are two boxed types, :tl:`User` and :tl:`Chat`.
A boxed :tl:`User` can only be the bare :tl:`user`, but the boxed :tl:`Chat` can be either a bare :tl:`chat` or a bare :tl:`channel`. A boxed :tl:`User` can only be the bare :tl:`user`, but the boxed :tl:`Chat` can be either a bare :tl:`chat` or a bare :tl:`channel`.
@ -63,33 +67,34 @@ In Telethon:
Telethon classes aim to map to similar concepts in official applications. Telethon classes aim to map to similar concepts in official applications.
Bot API chat Bot API Peer
------------ ------------
The Bot API follows a certain convention when it comes to identifiers: The Bot API does not use the word "peer", but instead opts to use "chat" and "user" only, despite chats also being able to reference users.
The Bot API follows a certain convention when it comes to chat and user identifiers:
* User IDs are positive. * User IDs are positive.
* Chat IDs are negative. * Chat IDs are negative.
* Channel IDs are *also* negative, but are prefixed by ``-100``. * Channel IDs are *also* negative, but are prefixed by ``-100``.
Telethon encourages the use of :class:`~types.PeerRef` instead of naked identifiers. Telethon does not support Bot API's formatted identifiers, and instead expects you to create the appropriated :class:`~types.PeerRef`:
As a reminder, negative identifiers are not supported in Telethon's chat-like parameters.
If you got an Bot API-style ID from somewhere else, you will need to explicitly say what type it is:
.. code-block:: python .. code-block:: python
# If -1001234 is your ID... from telethon.types import UserRef, GroupRef, ChannelRef
from telethon.types import PackedChat, PackedType
chat = PackedChat(PackedType.BROADCAST, 1234, None) user = UserRef(123) # user_id 123 from bot API becomes 123
# ...you need to explicitly create a PackedChat with id=1234 and set the corresponding type (a channel). group = GroupRef(456) # chat_id -456 from bot API becomes 456
# The access hash (see below) will be None, which may or may not work. channel = ChannelRef(789) # chat_id -100789 from bot API becomes 789
While using a Telethon Client logged in to a bot account, the above may work for certain methods.
However, user accounts often require what's known as an "access hash", obtained by encountering the peer first.
Encountering chats Encountering peers
------------------ ------------------
The way you encounter chats in Telethon is no different from official clients. The way you encounter peers in Telethon is no different from official clients.
If you: If you:
* …have joined a group or channel, or have sent private messages to some user, you can :meth:`~Client.get_dialogs`. * …have joined a group or channel, or have sent private messages to some user, you can :meth:`~Client.get_dialogs`.
@ -99,21 +104,19 @@ If you:
* …are a bot responding to users, you will be able to access the :attr:`types.Message.sender`. * …are a bot responding to users, you will be able to access the :attr:`types.Message.sender`.
Chats access hash Access hashes and authorizations
----------------- --------------------------------
Users, supergroups and channels all need an :term:`access hash`. Users, supergroups and channels all need an :term:`access hash`.
This value is proof that you're authorized to access the peer in question.
This value is also account-bound.
You cannot obtain an :term:`access hash` in Account-A and use it in Account-B.
In Telethon, the :class:`~types.PeerRef` is the recommended way to deal with the identifier-hash pairs. In Telethon, the :class:`~types.PeerRef` is the recommended way to deal with the identifier-authorization pairs.
This compact type can be used anywhere a chat is expected. This compact type can be used anywhere a peer is expected.
It's designed to be easy to store and cache in any way your application chooses. It's designed to be easy to store and cache in any way your application chooses.
You can easily serialize it to a string and back via ``str(ref)`` and :meth:`types.PeerRef.from_str`.
Bot accounts can get away with an invalid :term:`access hash` for certain operations under certain conditions. Bot accounts can get away with an invalid :term:`access hash` for certain operations under certain conditions.
The same is true for user accounts, although to a lesser extent. The same is true for user accounts, although to a lesser extent.
When you create a :class:`~types.PeerRef` without specifying an authorization, a bogus :term:`access hash` will be used.
When using just the identifier to refer to a chat, Telethon will attempt to retrieve its hash from its in-memory cache.
If this fails, an invalid hash will be used. This may or may not make the API call succeed.
For this reason, it is recommended that you always use :class:`~types.PeerRef` instead.
Remember that an :term:`access hash` is account-bound.
You cannot obtain an :term:`access hash` in Account-A and use it in Account-B.

View File

@ -444,20 +444,21 @@ Telegram may still choose to send their ``min`` version with only basic details.
But it means you don't have to remember 5 different ways of using chats. But it means you don't have to remember 5 different ways of using chats.
To replace the concept of "input chats", v2 introduces :class:`types.PeerRef`. To replace the concept of "input chats", v2 introduces :class:`types.PeerRef`.
A "packed chat" is a chat with *just* enough information that you can use it without relying on Telethon's cache. A "peer" represents either a :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`, much like Telegram's :tl:`Peer`.
A "peer reference" represents *just* enough information to reference that peer without relying on Telethon's cache.
This is the most efficient way to call methods like :meth:`Client.send_message` too. This is the most efficient way to call methods like :meth:`Client.send_message` too.
The concept of "marked IDs" also no longer exists. The concept of "marked IDs" also no longer exists.
This means v2 no longer supports the ``-`` or ``-100`` prefixes on identifiers. This means v2 no longer supports the ``-`` or ``-100`` prefixes on identifiers.
:tl:`Peer`-wrapping is gone, too. Using the raw :tl:`Peer` to wrap the identifiers is gone, too.
Instead, you're strongly encouraged to use :class:`types.PeerRef` instances. Instead, you're strongly encouraged to use :class:`types.PeerRef` instances.
The concepts of of "entity" or "peer" are unified to simply :term:`chat`. The concepts of of "entity" or "peer" are unified to :term:`peer`.
Overall, dealing with users, groups and channels should feel a lot more natural. Overall, dealing with users, groups and channels should feel a lot more natural.
.. seealso:: .. seealso::
In-depth explanation for :doc:`/concepts/chats`. In-depth explanation for :doc:`/concepts/peers`.
Other methods like ``client.get_peer_id``, ``client.get_input_entity`` and ``client.get_entity`` are gone too. Other methods like ``client.get_peer_id``, ``client.get_input_entity`` and ``client.get_entity`` are gone too.

View File

@ -78,13 +78,13 @@ Concepts
A more in-depth explanation of some of the concepts and words used in Telethon. A more in-depth explanation of some of the concepts and words used in Telethon.
:doc:`‣ Start reading Chat concept <concepts/chats>` :doc:`‣ Start reading Chat concept <concepts/peers>`
.. toctree:: .. toctree::
:hidden: :hidden:
:caption: Concepts :caption: Concepts
concepts/chats concepts/peers
concepts/updates concepts/updates
concepts/messages concepts/messages
concepts/sessions concepts/sessions

View File

@ -118,6 +118,16 @@ Private definitions
New-type wrapper around :class:`int` used as a message identifier. 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 .. currentmodule:: telethon._impl.mtsender.sender
.. autoclass:: AsyncReader .. autoclass:: AsyncReader

View File

@ -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 id=user.id, dc=client._sender.dc_id, bot=user.bot, username=user.username
) )
packed = user.pack() client._chat_hashes.set_self_user(user.id, user.bot)
assert packed is not None
client._chat_hashes.set_self_user(packed)
try: try:
state = await client(functions.updates.get_state()) state = await client(functions.updates.get_state())

View File

@ -3,8 +3,9 @@ from __future__ import annotations
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from typing import TYPE_CHECKING, Optional, Self from typing import TYPE_CHECKING, Optional, Self
from ...tl import abcs, functions, types from ...session import PeerRef, UserRef
from ..types import ChatLike, InlineResult, NoPublicConstructor from ...tl import functions, types
from ..types import InlineResult, NoPublicConstructor, Peer, User
if TYPE_CHECKING: if TYPE_CHECKING:
from .client import Client from .client import Client
@ -16,12 +17,12 @@ class InlineResults(metaclass=NoPublicConstructor):
client: Client, client: Client,
bot: types.InputUser, bot: types.InputUser,
query: str, query: str,
chat: abcs.InputPeer, peer: Optional[PeerRef],
): ):
self._client = client self._client = client
self._bot = bot self._bot = bot
self._query = query self._query = query
self._peer = chat or types.InputPeerEmpty() self._peer = peer
self._offset: Optional[str] = "" self._offset: Optional[str] = ""
self._buffer: list[InlineResult] = [] self._buffer: list[InlineResult] = []
self._done = False self._done = False
@ -37,7 +38,11 @@ class InlineResults(metaclass=NoPublicConstructor):
result = await self._client( result = await self._client(
functions.messages.get_inline_bot_results( functions.messages.get_inline_bot_results(
bot=self._bot, bot=self._bot,
peer=self._peer, peer=(
self._peer._to_input_peer()
if self._peer
else types.InputPeerEmpty()
),
geo_point=None, geo_point=None,
query=self._query, query=self._query,
offset=self._offset, offset=self._offset,
@ -61,13 +66,16 @@ class InlineResults(metaclass=NoPublicConstructor):
async def inline_query( 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]: ) -> 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( return InlineResults._create(
self, self,
packed_bot._to_input_user(), bot._ref._to_input_user(),
query, query,
packed_chat._to_input_peer() if packed_chat else types.InputPeerEmpty(), peer._ref if peer else None,
) )

View File

@ -3,16 +3,19 @@ from __future__ import annotations
import datetime import datetime
from typing import TYPE_CHECKING, Optional, Sequence from typing import TYPE_CHECKING, Optional, Sequence
from ...session import PeerRef from ...session import ChannelRef, GroupRef, PeerRef, UserRef
from ...tl import abcs, functions, types from ...tl import functions, types
from ..types import ( from ..types import (
AdminRight, AdminRight,
AsyncList, AsyncList,
ChatLike, Channel,
ChatRestriction, ChatRestriction,
File, File,
Group,
Participant, Participant,
Peer,
RecentAction, RecentAction,
User,
build_chat_map, build_chat_map,
) )
from .messages import SearchList from .messages import SearchList
@ -25,23 +28,19 @@ class ParticipantList(AsyncList[Participant]):
def __init__( def __init__(
self, self,
client: Client, client: Client,
chat: ChatLike, peer: ChannelRef | GroupRef,
): ):
super().__init__() super().__init__()
self._client = client self._client = client
self._chat = chat self._peer = peer
self._packed: Optional[PeerRef] = None
self._offset = 0 self._offset = 0
self._seen: set[int] = set() self._seen: set[int] = set()
async def _fetch_next(self) -> None: async def _fetch_next(self) -> None:
if self._packed is None: if isinstance(self._peer, ChannelRef):
self._packed = await self._client._resolve_to_packed(self._chat)
if self._packed.is_channel():
chanp = await self._client( chanp = await self._client(
functions.channels.get_participants( functions.channels.get_participants(
channel=self._packed._to_input_channel(), channel=self._peer._to_input_channel(),
filter=types.ChannelParticipantsRecent(), filter=types.ChannelParticipantsRecent(),
offset=self._offset, offset=self._offset,
limit=200, limit=200,
@ -55,7 +54,7 @@ class ParticipantList(AsyncList[Participant]):
seen_count = len(self._seen) seen_count = len(self._seen)
for p in chanp.participants: for p in chanp.participants:
part = Participant._from_raw_channel( part = Participant._from_raw_channel(
self._client, self._packed, p, chat_map self._client, self._peer, p, chat_map
) )
pid = part._peer_id() pid = part._peer_id()
if pid not in self._seen: if pid not in self._seen:
@ -66,9 +65,9 @@ class ParticipantList(AsyncList[Participant]):
self._offset += len(chanp.participants) self._offset += len(chanp.participants)
self._done = len(self._seen) == seen_count self._done = len(self._seen) == seen_count
elif self._packed.is_chat(): else:
chatp = await self._client( 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, types.messages.ChatFull)
assert isinstance(chatp.full_chat, types.ChatFull) assert isinstance(chatp.full_chat, types.ChatFull)
@ -81,48 +80,45 @@ class ParticipantList(AsyncList[Participant]):
self._buffer.append( self._buffer.append(
Participant._from_raw_chat( Participant._from_raw_chat(
self._client, self._client,
self._packed, self._peer,
participants.self_participant, participants.self_participant,
chat_map, chat_map,
) )
) )
elif isinstance(participants, types.ChatParticipants): elif isinstance(participants, types.ChatParticipants):
self._buffer.extend( 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 for p in participants.participants
) )
self._total = len(self._buffer) self._total = len(self._buffer)
self._done = True self._done = True
else:
raise TypeError("can only get participants from channels and groups")
def get_participants(self: Client, chat: ChatLike) -> AsyncList[Participant]: def get_participants(
return ParticipantList(self, chat) self: Client, chat: Group | Channel | GroupRef | ChannelRef, /
) -> AsyncList[Participant]:
return ParticipantList(self, chat._ref)
class RecentActionList(AsyncList[RecentAction]): class RecentActionList(AsyncList[RecentAction]):
def __init__( def __init__(
self, self,
client: Client, client: Client,
chat: ChatLike, peer: ChannelRef | GroupRef,
): ):
super().__init__() super().__init__()
self._client = client self._client = client
self._chat = chat self._peer = peer
self._peer: Optional[types.InputChannel] = None
self._offset = 0 self._offset = 0
async def _fetch_next(self) -> None: async def _fetch_next(self) -> None:
if self._peer is None: if not isinstance(self._peer, ChannelRef):
self._peer = ( return # small group chats have no recent actions
await self._client._resolve_to_packed(self._chat)
)._to_input_channel()
result = await self._client( result = await self._client(
functions.channels.get_admin_log( functions.channels.get_admin_log(
channel=self._peer, channel=self._peer._to_input_channel(),
q="", q="",
min_id=0, min_id=0,
max_id=self._offset, max_id=self._offset,
@ -141,34 +137,28 @@ class RecentActionList(AsyncList[RecentAction]):
self._offset = min(e.id for e in self._buffer) self._offset = min(e.id for e in self._buffer)
def get_admin_log(self: Client, chat: ChatLike) -> AsyncList[RecentAction]: def get_admin_log(
return RecentActionList(self, chat) self: Client, chat: Group | Channel | GroupRef | ChannelRef, /
) -> AsyncList[RecentAction]:
return RecentActionList(self, chat._ref)
class ProfilePhotoList(AsyncList[File]): class ProfilePhotoList(AsyncList[File]):
def __init__( def __init__(
self, self,
client: Client, client: Client,
chat: ChatLike, peer: PeerRef,
): ):
super().__init__() super().__init__()
self._client = client self._client = client
self._chat = chat self._peer = peer
self._peer: Optional[abcs.InputPeer] = None
self._search_iter: Optional[SearchList] = None self._search_iter: Optional[SearchList] = None
async def _fetch_next(self) -> None: async def _fetch_next(self) -> None:
if self._peer is None: if isinstance(self._peer, UserRef):
self._peer = (
await self._client._resolve_to_packed(self._chat)
)._to_input_peer()
if isinstance(self._peer, types.InputPeerUser):
result = await self._client( result = await self._client(
functions.photos.get_user_photos( functions.photos.get_user_photos(
user_id=types.InputUser( user_id=self._peer._to_input_user(),
user_id=self._peer.user_id, access_hash=self._peer.access_hash
),
offset=0, offset=0,
max_id=0, max_id=0,
limit=0, limit=0,
@ -191,86 +181,86 @@ class ProfilePhotoList(AsyncList[File]):
) )
def get_profile_photos(self: Client, chat: ChatLike) -> AsyncList[File]: def get_profile_photos(self: Client, peer: Peer | PeerRef, /) -> AsyncList[File]:
return ProfilePhotoList(self, chat) return ProfilePhotoList(self, peer._ref)
async def set_participant_admin_rights( 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: ) -> None:
packed = await self._resolve_to_packed(chat) chat = chat._ref
participant = await self._resolve_to_packed(user) user = participant._ref
if isinstance(chat, ChannelRef):
if packed.is_channel():
admin_rights = AdminRight._set_to_raw(set(rights)) admin_rights = AdminRight._set_to_raw(set(rights))
await self( await self(
functions.channels.edit_admin( functions.channels.edit_admin(
channel=packed._to_input_channel(), channel=chat._to_input_channel(),
user_id=participant._to_input_user(), user_id=user._to_input_user(),
admin_rights=admin_rights, admin_rights=admin_rights,
rank="", rank="",
) )
) )
elif packed.is_chat(): else:
await self( await self(
functions.messages.edit_chat_admin( functions.messages.edit_chat_admin(
chat_id=packed.id, chat_id=chat._to_input_chat(),
user_id=participant._to_input_user(), user_id=user._to_input_user(),
is_admin=bool(rights), is_admin=bool(rights),
) )
) )
else:
raise TypeError(f"Cannot set admin rights in {packed.ty}")
async def set_participant_restrictions( async def set_participant_restrictions(
self: Client, self: Client,
chat: ChatLike, chat: Group | Channel | GroupRef | ChannelRef,
user: ChatLike, /,
participant: Peer | PeerRef,
restrictions: Sequence[ChatRestriction], restrictions: Sequence[ChatRestriction],
*, *,
until: Optional[datetime.datetime] = None, until: Optional[datetime.datetime] = None,
) -> None: ) -> None:
packed = await self._resolve_to_packed(chat) chat = chat._ref
participant = await self._resolve_to_packed(user) peer = participant._ref
if packed.is_channel(): if isinstance(chat, ChannelRef):
banned_rights = ChatRestriction._set_to_raw( banned_rights = ChatRestriction._set_to_raw(
set(restrictions), set(restrictions),
until_date=int(until.timestamp()) if until else 0x7FFFFFFF, until_date=int(until.timestamp()) if until else 0x7FFFFFFF,
) )
await self( await self(
functions.channels.edit_banned( functions.channels.edit_banned(
channel=packed._to_input_channel(), channel=chat._to_input_channel(),
participant=participant._to_input_peer(), participant=peer._to_input_peer(),
banned_rights=banned_rights, banned_rights=banned_rights,
) )
) )
elif packed.is_chat(): elif isinstance(peer, UserRef):
if restrictions: if restrictions:
await self( await self(
functions.messages.delete_chat_user( functions.messages.delete_chat_user(
revoke_history=ChatRestriction.VIEW_MESSAGES in restrictions, revoke_history=ChatRestriction.VIEW_MESSAGES in restrictions,
chat_id=packed.id, chat_id=chat._to_input_chat(),
user_id=participant._to_input_user(), user_id=peer._to_input_user(),
) )
) )
else:
raise TypeError(f"Cannot set banned rights in {packed.ty}")
async def set_chat_default_restrictions( async def set_chat_default_restrictions(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
restrictions: Sequence[ChatRestriction], restrictions: Sequence[ChatRestriction],
*, *,
until: Optional[datetime.datetime] = None, until: Optional[datetime.datetime] = None,
) -> None: ) -> None:
peer = (await self._resolve_to_packed(chat))._to_input_peer()
banned_rights = ChatRestriction._set_to_raw( banned_rights = ChatRestriction._set_to_raw(
set(restrictions), int(until.timestamp()) if until else 0x7FFFFFFF set(restrictions), int(until.timestamp()) if until else 0x7FFFFFFF
) )
await self( await self(
functions.messages.edit_chat_default_banned_rights( functions.messages.edit_chat_default_banned_rights(
peer=peer, banned_rights=banned_rights peer=chat._ref._to_input_peer(), banned_rights=banned_rights
) )
) )

View File

@ -9,14 +9,17 @@ from typing import Any, Literal, Optional, Self, Sequence, Type, TypeVar
from ....version import __version__ as default_version from ....version import __version__ as default_version
from ...mtsender import Connector, Sender from ...mtsender import Connector, Sender
from ...session import ( from ...session import (
ChannelRef,
ChatHashCache, ChatHashCache,
DataCenter, DataCenter,
GroupRef,
MemorySession, MemorySession,
MessageBox, MessageBox,
PeerRef, PeerRef,
Session, Session,
SqliteSession, SqliteSession,
Storage, Storage,
UserRef,
) )
from ...tl import Request, abcs from ...tl import Request, abcs
from ..events import Event from ..events import Event
@ -25,11 +28,12 @@ from ..types import (
AdminRight, AdminRight,
AlbumBuilder, AlbumBuilder,
AsyncList, AsyncList,
ChatLike, Channel,
ChatRestriction, ChatRestriction,
Dialog, Dialog,
Draft, Draft,
File, File,
Group,
InFileLike, InFileLike,
InlineResult, InlineResult,
LoginToken, LoginToken,
@ -103,14 +107,7 @@ from .updates import (
remove_event_handler, remove_event_handler,
set_handler_filter, set_handler_filter,
) )
from .users import ( from .users import get_contacts, get_me, resolve_peers, resolve_phone, resolve_username
get_chats,
get_contacts,
get_me,
input_to_peer,
resolve_to_packed,
resolve_username,
)
Return = TypeVar("Return") Return = TypeVar("Return")
T = TypeVar("T") T = TypeVar("T")
@ -252,9 +249,9 @@ class Client:
self._message_box = MessageBox(base_logger=base_logger) self._message_box = MessageBox(base_logger=base_logger)
self._chat_hashes = ChatHashCache(None) self._chat_hashes = ChatHashCache(None)
self._last_update_limit_warn: Optional[float] = None self._last_update_limit_warn: Optional[float] = None
self._updates: asyncio.Queue[ self._updates: asyncio.Queue[tuple[abcs.Update, dict[int, Peer]]] = (
tuple[abcs.Update, dict[int, Peer]] asyncio.Queue(maxsize=self._config.update_queue_limit or 0)
] = asyncio.Queue(maxsize=self._config.update_queue_limit or 0) )
self._dispatcher: Optional[asyncio.Task[None]] = None self._dispatcher: Optional[asyncio.Task[None]] = None
self._handlers: dict[ self._handlers: dict[
Type[Event], list[tuple[Callable[[Any], Awaitable[Any]], Optional[Filter]]] Type[Event], list[tuple[Callable[[Any], Awaitable[Any]], Optional[Filter]]]
@ -269,6 +266,7 @@ class Client:
def add_event_handler( def add_event_handler(
self, self,
handler: Callable[[Event], Awaitable[Any]], handler: Callable[[Event], Awaitable[Any]],
/,
event_cls: Type[Event], event_cls: Type[Event],
filter: Optional[Filter] = None, filter: Optional[Filter] = None,
) -> None: ) -> None:
@ -387,7 +385,7 @@ class Client:
""" """
await connect(self) await connect(self)
async def delete_dialog(self, chat: ChatLike) -> None: async def delete_dialog(self, dialog: Peer | PeerRef, /) -> None:
""" """
Delete a dialog. 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. Note that bot accounts do not have dialogs, so this method will fail when used in a bot account.
:param chat: :param dialog:
The :term:`chat` representing the dialog to delete. The :term:`peer` representing the dialog to delete.
.. rubric:: Example .. rubric:: Example
@ -409,16 +407,16 @@ class Client:
# You've realized you're more of a cat person # You've realized you're more of a cat person
await client.delete_dialog(dialog.chat) await client.delete_dialog(dialog.chat)
""" """
await delete_dialog(self, chat) await delete_dialog(self, dialog)
async def delete_messages( 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: ) -> int:
""" """
Delete messages. Delete messages.
:param chat: :param chat:
The :term:`chat` where the messages are. The :term:`peer` where the messages are.
.. warning:: .. warning::
@ -468,7 +466,7 @@ class Client:
""" """
await disconnect(self) 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. Download a file.
@ -504,7 +502,8 @@ class Client:
async def edit_draft( async def edit_draft(
self, self,
chat: ChatLike, peer: Peer | PeerRef,
/,
text: Optional[str] = None, text: Optional[str] = None,
*, *,
markdown: 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 ``""``. This can also be used to clear the draft by setting the text to an empty string ``""``.
:param chat: :param peer:
The :term:`chat` where the draft will be saved to. The :term:`peer` where the draft will be saved to.
:param text: See :ref:`formatting`. :param text: See :ref:`formatting`.
:param markdown: See :ref:`formatting`. :param markdown: See :ref:`formatting`.
@ -552,7 +551,7 @@ class Client:
""" """
return await edit_draft( return await edit_draft(
self, self,
chat, peer,
text, text,
markdown=markdown, markdown=markdown,
html=html, html=html,
@ -562,7 +561,8 @@ class Client:
async def edit_message( async def edit_message(
self, self,
chat: ChatLike, chat: Peer | PeerRef,
/,
message_id: int, message_id: int,
*, *,
text: Optional[str] = None, text: Optional[str] = None,
@ -575,7 +575,7 @@ class Client:
Edit a message. Edit a message.
:param chat: :param chat:
The :term:`chat` where the message to edit is. The :term:`peer` where the message to edit is.
:param message_id: :param message_id:
The identifier of the message to edit. The identifier of the message to edit.
@ -617,19 +617,19 @@ class Client:
) )
async def forward_messages( 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]: ) -> list[Message]:
""" """
Forward messages from one :term:`chat` to another. Forward messages from one :term:`peer` to another.
:param target: :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: :param message_ids:
The list of message identifiers to forward. The list of message identifiers to forward.
:param source: :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. :return: The forwarded messages.
@ -651,16 +651,18 @@ class Client:
""" """
return await forward_messages(self, target, message_ids, source) 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. 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". The returned actions are also known as "admin log events".
:param chat: :param chat:
The :term:`chat` to fetch recent actions from. The :term:`peer` to fetch recent actions from.
:return: The recent actions. :return: The recent actions.
@ -674,42 +676,6 @@ class Client:
""" """
return get_admin_log(self, chat) 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]: def get_contacts(self) -> AsyncList[User]:
""" """
Get the users in your contact list. Get the users in your contact list.
@ -763,7 +729,7 @@ class Client:
""" """
return get_drafts(self) 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`. 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) return get_file_bytes(self, media)
def get_handler_filter( def get_handler_filter(
self, handler: Callable[[Event], Awaitable[Any]] self, handler: Callable[[Event], Awaitable[Any]], /
) -> Optional[Filter]: ) -> Optional[Filter]:
""" """
Get the filter associated to the given event handler. Get the filter associated to the given event handler.
@ -841,19 +807,20 @@ class Client:
def get_messages( def get_messages(
self, self,
chat: ChatLike, chat: Peer | PeerRef,
/,
limit: Optional[int] = None, limit: Optional[int] = None,
*, *,
offset_id: Optional[int] = None, offset_id: Optional[int] = None,
offset_date: Optional[datetime.datetime] = None, offset_date: Optional[datetime.datetime] = None,
) -> AsyncList[Message]: ) -> 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. The returned iterator can be :func:`reversed` to fetch from the first to the last instead.
:param chat: :param chat:
The :term:`chat` where the messages should be fetched from. The :term:`peer` where the messages should be fetched from.
:param limit: :param limit:
How many messages to fetch at most. How many messages to fetch at most.
@ -891,13 +858,13 @@ class Client:
) )
def get_messages_with_ids( def get_messages_with_ids(
self, chat: ChatLike, message_ids: list[int] self, chat: Peer | PeerRef, /, message_ids: list[int]
) -> AsyncList[Message]: ) -> AsyncList[Message]:
""" """
Get the full message objects from the corresponding message identifiers. Get the full message objects from the corresponding message identifiers.
:param chat: :param chat:
The :term:`chat` where the message to fetch is. The :term:`peer` where the message to fetch is.
:param message_ids: :param message_ids:
The message identifiers of the messages to fetch. The message identifiers of the messages to fetch.
@ -916,7 +883,9 @@ class Client:
""" """
return get_messages_with_ids(self, chat, message_ids) 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. 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. There is no way to bypass this.
:param chat: :param chat:
The :term:`chat` to fetch participants from. The :term:`peer` to fetch participants from.
:return: The participants. :return: The participants.
@ -940,12 +909,12 @@ class Client:
""" """
return get_participants(self, chat) 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. Get the profile pictures set in a chat, or user avatars.
:param chat: :param peer:
The :term:`chat` to fetch the profile photo files from. The :term:`peer` to fetch the profile photo files from.
:return: The photo files. :return: The photo files.
@ -958,10 +927,15 @@ class Client:
await client.download(photo, f'{i}.jpg') await client.download(photo, f'{i}.jpg')
i += 1 i += 1
""" """
return get_profile_photos(self, chat) return get_profile_photos(self, peer)
async def inline_query( 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]: ) -> AsyncIterator[InlineResult]:
""" """
Perform a *@bot inline query*. Perform a *@bot inline query*.
@ -975,7 +949,7 @@ class Client:
:param query: :param query:
The query string to send to the bot. The query string to send to the bot.
:param chat: :param peer:
Where the query is being made and will be sent. Where the query is being made and will be sent.
Some bots display different results based on the type of chat. Some bots display different results based on the type of chat.
@ -997,7 +971,7 @@ class Client:
if i == 10: if i == 10:
break # did not find 'keyword' in the first few results 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( async def interactive_login(
self, phone_or_token: Optional[str] = None, *, password: Optional[str] = None self, phone_or_token: Optional[str] = None, *, password: Optional[str] = None
@ -1052,7 +1026,7 @@ class Client:
return await is_authorized(self) return await is_authorized(self)
def on( def on(
self, event_cls: Type[Event], filter: Optional[Filter] = None self, event_cls: Type[Event], /, filter: Optional[Filter] = None
) -> Callable[ ) -> Callable[
[Callable[[Event], Awaitable[Any]]], Callable[[Event], Awaitable[Any]] [Callable[[Event], Awaitable[Any]]], Callable[[Event], Awaitable[Any]]
]: ]:
@ -1093,12 +1067,12 @@ class Client:
""" """
return on(self, event_cls, filter) 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. Pin a message to be at the top.
:param chat: :param chat:
The :term:`chat` where the message to pin is. The :term:`peer` where the message to pin is.
:param message_id: :param message_id:
The identifier of the message to pin. The identifier of the message to pin.
@ -1144,7 +1118,7 @@ class Client:
return prepare_album(self) return prepare_album(self)
async def read_message( async def read_message(
self, chat: ChatLike, message_id: int | Literal["all"] self, chat: Peer | PeerRef, /, message_id: int | Literal["all"]
) -> None: ) -> None:
""" """
Mark messages as read. Mark messages as read.
@ -1176,7 +1150,9 @@ class Client:
""" """
await read_message(self, chat, message_id) 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. Remove the handler as a function to be called when events occur.
This is simply the opposite of :meth:`add_event_handler`. This is simply the opposite of :meth:`add_event_handler`.
@ -1227,16 +1203,60 @@ class Client:
""" """
return await request_login_code(self, phone) 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. This method is rather expensive to call.
It is recommended to use it once and then :meth:`types.Peer.pack` the result. It is recommended to use it once and then store the :attr:`types.Peer.ref`.
The packed chat can then be used (and re-fetched) more cheaply.
:param phone:
The phone number "+1 23 456" to resolve.
The phone number must contain the `International Calling Code <https://en.wikipedia.org/wiki/List_of_mobile_telephone_prefixes_by_country>`_.
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: :param username:
The public "@username" to resolve. The public "@username" to resolve.
You do not need to use include the ``'@'`` prefix.
Links cannot be used.
:return: The matching chat. :return: The matching chat.
@ -1268,9 +1288,6 @@ class Client:
Perform a global message search. Perform a global message search.
This is used to search messages in no particular chat (i.e. everywhere possible). 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: :param limit:
How many messages to fetch at most. How many messages to fetch at most.
@ -1301,7 +1318,8 @@ class Client:
def search_messages( def search_messages(
self, self,
chat: ChatLike, chat: Peer | PeerRef,
/,
limit: Optional[int] = None, limit: Optional[int] = None,
*, *,
query: Optional[str] = None, query: Optional[str] = None,
@ -1312,7 +1330,7 @@ class Client:
Search messages in a chat. Search messages in a chat.
:param chat: :param chat:
The :term:`chat` where messages will be searched. The :term:`peer` where messages will be searched.
:param limit: :param limit:
How many messages to fetch at most. How many messages to fetch at most.
@ -1344,12 +1362,13 @@ class Client:
async def send_audio( async def send_audio(
self, self,
chat: ChatLike, chat: Peer | PeerRef,
/,
file: str | Path | InFileLike | File, file: str | Path | InFileLike | File,
mime_type: Optional[str] = None,
*, *,
size: Optional[int] = None, size: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
mime_type: Optional[str] = None,
duration: Optional[float] = None, duration: Optional[float] = None,
voice: bool = False, voice: bool = False,
title: Optional[str] = None, title: Optional[str] = None,
@ -1367,7 +1386,7 @@ class Client:
duration, title and performer if they are not provided. duration, title and performer if they are not provided.
:param chat: :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 file: See :meth:`send_file`.
:param size: See :meth:`send_file`. :param size: See :meth:`send_file`.
@ -1391,9 +1410,9 @@ class Client:
self, self,
chat, chat,
file, file,
mime_type,
size=size, size=size,
name=name, name=name,
mime_type=mime_type,
duration=duration, duration=duration,
voice=voice, voice=voice,
title=title, title=title,
@ -1407,7 +1426,8 @@ class Client:
async def send_file( async def send_file(
self, self,
chat: ChatLike, chat: Peer | PeerRef,
/,
file: str | Path | InFileLike | File, file: str | Path | InFileLike | File,
*, *,
size: Optional[int] = None, size: Optional[int] = None,
@ -1430,7 +1450,7 @@ class Client:
caption_markdown: Optional[str] = None, caption_markdown: Optional[str] = None,
caption_html: Optional[str] = None, caption_html: Optional[str] = None,
reply_to: Optional[int] = 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: ) -> Message:
""" """
Send any type of file with any amount of attributes. 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. Unlike :meth:`send_photo`, image files will be sent as documents by default.
:param chat: :param chat:
The :term:`chat` where the message will be sent to. The :term:`peer` where the message will be sent to.
:param path: :param path:
A local file path or :class:`~telethon.types.File` to send. A local file path or :class:`~telethon.types.File` to send.
@ -1589,7 +1609,8 @@ class Client:
async def send_message( async def send_message(
self, self,
chat: ChatLike, chat: Peer | PeerRef,
/,
text: Optional[str | Message] = None, text: Optional[str | Message] = None,
*, *,
markdown: Optional[str] = None, markdown: Optional[str] = None,
@ -1602,7 +1623,7 @@ class Client:
Send a message. Send a message.
:param chat: :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 text: See :ref:`formatting`.
:param markdown: See :ref:`formatting`. :param markdown: See :ref:`formatting`.
@ -1636,7 +1657,8 @@ class Client:
async def send_photo( async def send_photo(
self, self,
chat: ChatLike, chat: Peer | PeerRef,
/,
file: str | Path | InFileLike | File, file: str | Path | InFileLike | File,
*, *,
size: Optional[int] = None, size: Optional[int] = None,
@ -1662,7 +1684,7 @@ class Client:
width and height if they are not provided. width and height if they are not provided.
:param chat: :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 file: See :meth:`send_file`.
:param size: See :meth:`send_file`. :param size: See :meth:`send_file`.
@ -1700,7 +1722,8 @@ class Client:
async def send_video( async def send_video(
self, self,
chat: ChatLike, chat: Peer | PeerRef,
/,
file: str | Path | InFileLike | File, file: str | Path | InFileLike | File,
*, *,
size: Optional[int] = None, size: Optional[int] = None,
@ -1716,7 +1739,7 @@ class Client:
caption_markdown: Optional[str] = None, caption_markdown: Optional[str] = None,
caption_html: Optional[str] = None, caption_html: Optional[str] = None,
reply_to: Optional[int] = 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: ) -> Message:
""" """
Send a video file. Send a video file.
@ -1725,7 +1748,7 @@ class Client:
duration, width and height if they are not provided. duration, width and height if they are not provided.
:param chat: :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 file: See :meth:`send_file`.
:param size: See :meth:`send_file`. :param size: See :meth:`send_file`.
@ -1768,7 +1791,8 @@ class Client:
async def set_chat_default_restrictions( async def set_chat_default_restrictions(
self, self,
chat: ChatLike, chat: Peer | PeerRef,
/,
restrictions: Sequence[ChatRestriction], restrictions: Sequence[ChatRestriction],
*, *,
until: Optional[datetime.datetime] = None, until: Optional[datetime.datetime] = None,
@ -1777,7 +1801,7 @@ class Client:
Set the default restrictions to apply to all participant in a chat. Set the default restrictions to apply to all participant in a chat.
:param chat: :param chat:
The :term:`chat` where the restrictions will be applied. The :term:`peer` where the restrictions will be applied.
:param restrictions: :param restrictions:
The sequence of restrictions to apply. The sequence of restrictions to apply.
@ -1810,6 +1834,7 @@ class Client:
def set_handler_filter( def set_handler_filter(
self, self,
handler: Callable[[Event], Awaitable[Any]], handler: Callable[[Event], Awaitable[Any]],
/,
filter: Optional[Filter] = None, filter: Optional[Filter] = None,
) -> None: ) -> None:
""" """
@ -1836,7 +1861,11 @@ class Client:
set_handler_filter(self, handler, filter) set_handler_filter(self, handler, filter)
async def set_participant_admin_rights( 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: ) -> None:
""" """
Set the administrator rights granted to the participant in the chat. 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. In this case, granting any right will make the user an administrator with all rights.
:param chat: :param chat:
The :term:`chat` where the rights will be granted. The :term:`peer` where the rights will be granted.
:param participant: :param participant:
The participant to promote to administrator, usually a :class:`types.User`. The participant to promote to administrator, usually a :class:`types.User`.
@ -1873,12 +1902,13 @@ class Client:
:meth:`telethon.types.Participant.set_admin_rights` :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( async def set_participant_restrictions(
self, self,
chat: ChatLike, chat: Group | Channel | GroupRef | ChannelRef,
user: ChatLike, /,
participant: Peer | PeerRef,
restrictions: Sequence[ChatRestriction], restrictions: Sequence[ChatRestriction],
*, *,
until: Optional[datetime.datetime] = None, 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. The participant's history will be revoked if the restriction to :attr:`~types.ChatRestriction.VIEW_MESSAGES` is applied.
:param chat: :param chat:
The :term:`chat` where the restrictions will be applied. The :term:`peer` where the restrictions will be applied.
:param participant: :param participant:
The participant to restrict or ban, usually a :class:`types.User`. The participant to restrict or ban, usually a :class:`types.User`.
@ -1929,7 +1959,9 @@ class Client:
:meth:`telethon.types.Participant.set_restrictions` :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: async def sign_in(self, token: LoginToken, code: str) -> User | PasswordToken:
""" """
@ -1977,13 +2009,13 @@ class Client:
await sign_out(self) await sign_out(self)
async def unpin_message( async def unpin_message(
self, chat: ChatLike, message_id: int | Literal["all"] self, chat: Peer | PeerRef, /, message_id: int | Literal["all"]
) -> None: ) -> None:
""" """
Unpin one or all messages from the top. Unpin one or all messages from the top.
:param chat: :param chat:
The :term:`chat` where the message pinned message is. The :term:`peer` where the message pinned message is.
:param message_id: :param message_id:
The identifier of the message to unpin, or ``'all'`` to unpin them all. The identifier of the message to unpin, or ``'all'`` to unpin them all.
@ -2014,16 +2046,10 @@ class Client:
def _build_message_map( def _build_message_map(
self, self,
result: abcs.Updates, result: abcs.Updates,
peer: Optional[abcs.InputPeer], peer: Optional[PeerRef],
) -> MessageMap: ) -> MessageMap:
return build_message_map(self, result, peer) 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( async def _upload(
self, fd: str | Path | InFileLike, size: Optional[int], name: Optional[str] self, fd: str | Path | InFileLike, size: Optional[int], name: Optional[str]
) -> tuple[abcs.InputFile, str]: ) -> tuple[abcs.InputFile, str]:

View File

@ -3,12 +3,13 @@ from __future__ import annotations
import time import time
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from ...session import PeerRef
from ...tl import functions, types from ...tl import functions, types
from ..types import ( from ..types import (
AsyncList, AsyncList,
ChatLike,
Dialog, Dialog,
Draft, Draft,
Peer,
build_chat_map, build_chat_map,
build_msg_map, build_msg_map,
parse_message, parse_message,
@ -59,8 +60,8 @@ def get_dialogs(self: Client) -> AsyncList[Dialog]:
return DialogList(self) return DialogList(self)
async def delete_dialog(self: Client, chat: ChatLike) -> None: async def delete_dialog(self: Client, dialog: Peer | PeerRef, /) -> None:
peer = (await self._resolve_to_packed(chat))._to_input_peer() peer = dialog._ref
if isinstance(peer, types.InputPeerChannel): if isinstance(peer, types.InputPeerChannel):
await self( await self(
functions.channels.leave_channel( functions.channels.leave_channel(
@ -119,7 +120,8 @@ def get_drafts(self: Client) -> AsyncList[Draft]:
async def edit_draft( async def edit_draft(
self: Client, self: Client,
chat: ChatLike, peer: Peer | PeerRef,
/,
text: Optional[str] = None, text: Optional[str] = None,
*, *,
markdown: Optional[str] = None, markdown: Optional[str] = None,
@ -127,8 +129,7 @@ async def edit_draft(
link_preview: bool = False, link_preview: bool = False,
reply_to: Optional[int] = None, reply_to: Optional[int] = None,
) -> Draft: ) -> Draft:
packed = await self._resolve_to_packed(chat) peer = peer._ref
peer = (await self._resolve_to_packed(chat))._to_input_peer()
message, entities = parse_message( message, entities = parse_message(
text=text, markdown=markdown, html=html, allow_empty=False text=text, markdown=markdown, html=html, allow_empty=False
) )
@ -138,7 +139,7 @@ async def edit_draft(
no_webpage=not link_preview, no_webpage=not link_preview,
reply_to_msg_id=reply_to, reply_to_msg_id=reply_to,
top_msg_id=None, top_msg_id=None,
peer=peer, peer=peer._to_input_peer(),
message=message, message=message,
entities=entities, entities=entities,
) )
@ -147,7 +148,7 @@ async def edit_draft(
return Draft._from_raw( return Draft._from_raw(
client=self, client=self,
peer=packed._to_peer(), peer=peer._to_peer(),
top_msg_id=0, top_msg_id=0,
draft=types.DraftMessage( draft=types.DraftMessage(
no_webpage=not link_preview, no_webpage=not link_preview,

View File

@ -6,16 +6,17 @@ from inspect import isawaitable
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from ...session import PeerRef
from ...tl import abcs, functions, types from ...tl import abcs, functions, types
from ..types import ( from ..types import (
AlbumBuilder, AlbumBuilder,
AsyncList, AsyncList,
ChatLike,
File, File,
InFileLike, InFileLike,
Message, Message,
OutFileLike, OutFileLike,
OutWrapper, OutWrapper,
Peer,
) )
from ..types import buttons as btns from ..types import buttons as btns
from ..types import ( from ..types import (
@ -44,7 +45,8 @@ def prepare_album(self: Client) -> AlbumBuilder:
async def send_photo( async def send_photo(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
file: str | Path | InFileLike | File, file: str | Path | InFileLike | File,
*, *,
size: Optional[int] = None, size: Optional[int] = None,
@ -83,12 +85,13 @@ async def send_photo(
async def send_audio( async def send_audio(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
file: str | Path | InFileLike | File, file: str | Path | InFileLike | File,
mime_type: Optional[str] = None,
*, *,
size: Optional[int] = None, size: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
mime_type: Optional[str] = None,
duration: Optional[float] = None, duration: Optional[float] = None,
voice: bool = False, voice: bool = False,
title: Optional[str] = None, title: Optional[str] = None,
@ -120,7 +123,8 @@ async def send_audio(
async def send_video( async def send_video(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
file: str | Path | InFileLike | File, file: str | Path | InFileLike | File,
*, *,
size: Optional[int] = None, size: Optional[int] = None,
@ -161,7 +165,8 @@ async def send_video(
async def send_file( async def send_file(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
file: str | Path | InFileLike | File, file: str | Path | InFileLike | File,
*, *,
size: Optional[int] = None, size: Optional[int] = None,
@ -289,14 +294,13 @@ async def send_file(
async def do_send_file( async def do_send_file(
client: Client, client: Client,
chat: ChatLike, chat: Peer | PeerRef,
input_media: abcs.InputMedia, input_media: abcs.InputMedia,
message: str, message: str,
entities: Optional[list[abcs.MessageEntity]], entities: Optional[list[abcs.MessageEntity]],
reply_to: Optional[int], reply_to: Optional[int],
buttons: Optional[list[btns.Button] | list[list[btns.Button]]], buttons: Optional[list[btns.Button] | list[list[btns.Button]]],
) -> Message: ) -> Message:
peer = (await client._resolve_to_packed(chat))._to_input_peer()
random_id = generate_random_id() random_id = generate_random_id()
return client._build_message_map( return client._build_message_map(
await client( await client(
@ -306,7 +310,7 @@ async def do_send_file(
clear_draft=False, clear_draft=False,
noforwards=False, noforwards=False,
update_stickersets_order=False, update_stickersets_order=False,
peer=peer, peer=chat._ref._to_input_peer(),
reply_to=( reply_to=(
types.InputReplyToMessage(reply_to_msg_id=reply_to, top_msg_id=None) types.InputReplyToMessage(reply_to_msg_id=reply_to, top_msg_id=None)
if reply_to if reply_to
@ -321,7 +325,7 @@ async def do_send_file(
send_as=None, send_as=None,
) )
), ),
peer, chat._ref,
).with_random_id(random_id) ).with_random_id(random_id)
@ -452,11 +456,13 @@ class FileBytesList(AsyncList[bytes]):
self._done = len(result.bytes) < MAX_CHUNK_SIZE 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) 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) fd = OutWrapper(file)
try: try:
async for chunk in get_file_bytes(self, media): async for chunk in get_file_bytes(self, media):

View File

@ -4,9 +4,9 @@ import datetime
import sys import sys
from typing import TYPE_CHECKING, Literal, Optional, Self from typing import TYPE_CHECKING, Literal, Optional, Self
from ...session import PeerRef from ...session import ChannelRef, PeerRef
from ...tl import abcs, functions, types 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 buttons as btns
from ..types import generate_random_id, parse_message, peer_id from ..types import generate_random_id, parse_message, peer_id
@ -16,7 +16,8 @@ if TYPE_CHECKING:
async def send_message( async def send_message(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
text: Optional[str | Message] = None, text: Optional[str | Message] = None,
*, *,
markdown: Optional[str] = None, markdown: Optional[str] = None,
@ -25,8 +26,6 @@ async def send_message(
reply_to: Optional[int] = 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]]] = None,
) -> Message: ) -> Message:
packed = await self._resolve_to_packed(chat)
peer = packed._to_input_peer()
random_id = generate_random_id() random_id = generate_random_id()
if isinstance(text, Message): if isinstance(text, Message):
@ -38,7 +37,7 @@ async def send_message(
clear_draft=False, clear_draft=False,
noforwards=not text.can_forward, noforwards=not text.can_forward,
update_stickersets_order=False, update_stickersets_order=False,
peer=peer, peer=chat._ref._to_input_peer(),
reply_to=( reply_to=(
types.InputReplyToMessage( types.InputReplyToMessage(
reply_to_msg_id=text.replied_message_id, top_msg_id=None reply_to_msg_id=text.replied_message_id, top_msg_id=None
@ -64,7 +63,7 @@ async def send_message(
clear_draft=False, clear_draft=False,
noforwards=False, noforwards=False,
update_stickersets_order=False, update_stickersets_order=False,
peer=peer, peer=chat._ref._to_input_peer(),
reply_to=( reply_to=(
types.InputReplyToMessage(reply_to_msg_id=reply_to, top_msg_id=None) types.InputReplyToMessage(reply_to_msg_id=reply_to, top_msg_id=None)
if reply_to if reply_to
@ -90,7 +89,7 @@ async def send_message(
if self._session.user if self._session.user
else None else None
), ),
peer_id=packed._to_peer(), peer_id=chat._ref._to_peer(),
reply_to=( reply_to=(
types.MessageReplyHeader( types.MessageReplyHeader(
reply_to_scheduled=False, reply_to_scheduled=False,
@ -109,12 +108,13 @@ async def send_message(
ttl_period=result.ttl_period, ttl_period=result.ttl_period,
) )
else: 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( async def edit_message(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
message_id: int, message_id: int,
*, *,
text: Optional[str] = None, text: Optional[str] = None,
@ -123,7 +123,6 @@ async def edit_message(
link_preview: bool = False, link_preview: bool = False,
buttons: Optional[list[btns.Button] | list[list[btns.Button]]] = None, buttons: Optional[list[btns.Button] | list[list[btns.Button]]] = None,
) -> Message: ) -> Message:
peer = (await self._resolve_to_packed(chat))._to_input_peer()
message, entities = parse_message( message, entities = parse_message(
text=text, markdown=markdown, html=html, allow_empty=False text=text, markdown=markdown, html=html, allow_empty=False
) )
@ -131,7 +130,7 @@ async def edit_message(
await self( await self(
functions.messages.edit_message( functions.messages.edit_message(
no_webpage=not link_preview, no_webpage=not link_preview,
peer=peer, peer=chat._ref._to_input_peer(),
id=message_id, id=message_id,
message=message, message=message,
media=None, media=None,
@ -140,18 +139,23 @@ async def edit_message(
schedule_date=None, schedule_date=None,
) )
), ),
peer, chat._ref,
).with_id(message_id) ).with_id(message_id)
async def delete_messages( 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: ) -> int:
packed_chat = await self._resolve_to_packed(chat) peer = chat._ref
if packed_chat.is_channel(): if isinstance(peer, ChannelRef):
affected = await self( affected = await self(
functions.channels.delete_messages( functions.channels.delete_messages(
channel=packed_chat._to_input_channel(), id=message_ids channel=peer._to_input_channel(), id=message_ids
) )
) )
else: else:
@ -163,10 +167,8 @@ async def delete_messages(
async def forward_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]: ) -> 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] random_ids = [generate_random_id() for _ in message_ids]
map = self._build_message_map( map = self._build_message_map(
await self( await self(
@ -177,16 +179,16 @@ async def forward_messages(
drop_author=False, drop_author=False,
drop_media_captions=False, drop_media_captions=False,
noforwards=False, noforwards=False,
from_peer=from_peer, from_peer=source._ref._to_input_peer(),
id=message_ids, id=message_ids,
random_id=random_ids, random_id=random_ids,
to_peer=to_peer, to_peer=target._ref._to_input_peer(),
top_msg_id=None, top_msg_id=None,
schedule_date=None, schedule_date=None,
send_as=None, send_as=None,
) )
), ),
to_peer, target._ref,
) )
return [map.with_random_id(id) for id in random_ids] return [map.with_random_id(id) for id in random_ids]
@ -239,7 +241,7 @@ class HistoryList(MessageList):
def __init__( def __init__(
self, self,
client: Client, client: Client,
chat: ChatLike, peer: PeerRef,
limit: int, limit: int,
*, *,
offset_id: int, offset_id: int,
@ -247,8 +249,7 @@ class HistoryList(MessageList):
): ):
super().__init__() super().__init__()
self._client = client self._client = client
self._chat = chat self._peer = peer
self._peer: Optional[abcs.InputPeer] = None
self._limit = limit self._limit = limit
self._offset_id = offset_id self._offset_id = offset_id
self._offset_date = offset_date self._offset_date = offset_date
@ -256,15 +257,10 @@ class HistoryList(MessageList):
self._done = limit <= 0 self._done = limit <= 0
async def _fetch_next(self) -> 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()
limit = min(max(self._limit, 1), 100) limit = min(max(self._limit, 1), 100)
result = await self._client( result = await self._client(
functions.messages.get_history( functions.messages.get_history(
peer=self._peer, peer=self._peer._to_input_peer(),
offset_id=self._offset_id, offset_id=self._offset_id,
offset_date=self._offset_date, offset_date=self._offset_date,
add_offset=-limit if self._reversed else 0, add_offset=-limit if self._reversed else 0,
@ -286,7 +282,7 @@ class HistoryList(MessageList):
def __reversed__(self) -> Self: def __reversed__(self) -> Self:
new = self.__class__( new = self.__class__(
self._client, self._client,
self._chat, self._peer,
self._limit, self._limit,
offset_id=1 if self._offset_id == 0 else self._offset_id, offset_id=1 if self._offset_id == 0 else self._offset_id,
offset_date=self._offset_date, offset_date=self._offset_date,
@ -298,7 +294,8 @@ class HistoryList(MessageList):
def get_messages( def get_messages(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
limit: Optional[int] = None, limit: Optional[int] = None,
*, *,
offset_id: Optional[int] = None, offset_id: Optional[int] = None,
@ -306,7 +303,7 @@ def get_messages(
) -> AsyncList[Message]: ) -> AsyncList[Message]:
return HistoryList( return HistoryList(
self, self,
chat, chat._ref,
sys.maxsize if limit is None else limit, sys.maxsize if limit is None else limit,
offset_id=offset_id or 0, offset_id=offset_id or 0,
offset_date=int(offset_date.timestamp()) if offset_date is not None else 0, offset_date=int(offset_date.timestamp()) if offset_date is not None else 0,
@ -317,25 +314,22 @@ class CherryPickedList(MessageList):
def __init__( def __init__(
self, self,
client: Client, client: Client,
chat: ChatLike, peer: PeerRef,
ids: list[int], ids: list[int],
): ):
super().__init__() super().__init__()
self._client = client self._client = client
self._chat = chat self._peer = peer
self._packed: Optional[PeerRef] = None
self._ids: list[abcs.InputMessage] = [types.InputMessageId(id=id) for id in ids] self._ids: list[abcs.InputMessage] = [types.InputMessageId(id=id) for id in ids]
async def _fetch_next(self) -> None: async def _fetch_next(self) -> None:
if not self._ids: if not self._ids:
return 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( result = await self._client(
functions.channels.get_messages( 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: else:
@ -349,17 +343,18 @@ class CherryPickedList(MessageList):
def get_messages_with_ids( def get_messages_with_ids(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
message_ids: list[int], message_ids: list[int],
) -> AsyncList[Message]: ) -> AsyncList[Message]:
return CherryPickedList(self, chat, message_ids) return CherryPickedList(self, chat._ref, message_ids)
class SearchList(MessageList): class SearchList(MessageList):
def __init__( def __init__(
self, self,
client: Client, client: Client,
chat: ChatLike, peer: PeerRef,
limit: int, limit: int,
*, *,
query: str, query: str,
@ -368,8 +363,7 @@ class SearchList(MessageList):
): ):
super().__init__() super().__init__()
self._client = client self._client = client
self._chat = chat self._peer = peer
self._peer: Optional[abcs.InputPeer] = None
self._limit = limit self._limit = limit
self._query = query self._query = query
self._filter = types.InputMessagesFilterEmpty() self._filter = types.InputMessagesFilterEmpty()
@ -377,14 +371,9 @@ class SearchList(MessageList):
self._offset_date = offset_date self._offset_date = offset_date
async def _fetch_next(self) -> 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()
result = await self._client( result = await self._client(
functions.messages.search( functions.messages.search(
peer=self._peer, peer=self._peer._to_input_peer(),
q=self._query, q=self._query,
from_id=None, from_id=None,
top_msg_id=None, top_msg_id=None,
@ -411,7 +400,8 @@ class SearchList(MessageList):
def search_messages( def search_messages(
self: Client, self: Client,
chat: ChatLike, chat: Peer | PeerRef,
/,
limit: Optional[int] = None, limit: Optional[int] = None,
*, *,
query: Optional[str] = None, query: Optional[str] = None,
@ -420,7 +410,7 @@ def search_messages(
) -> AsyncList[Message]: ) -> AsyncList[Message]:
return SearchList( return SearchList(
self, self,
chat, chat._ref,
sys.maxsize if limit is None else limit, sys.maxsize if limit is None else limit,
query=query or "", query=query or "",
offset_id=offset_id or 0, offset_id=offset_id or 0,
@ -475,8 +465,7 @@ class GlobalSearchList(MessageList):
self._offset_peer = types.InputPeerEmpty() self._offset_peer = types.InputPeerEmpty()
if last.peer_id and (chat := chat_map.get(peer_id(last.peer_id))): if last.peer_id and (chat := chat_map.get(peer_id(last.peer_id))):
if packed := chat.pack(): self._offset_peer = chat._ref._to_input_peer()
self._offset_peer = packed._to_input_peer()
def search_all_messages( def search_all_messages(
@ -496,54 +485,62 @@ def search_all_messages(
) )
async def pin_message(self: Client, chat: ChatLike, message_id: int) -> Message: async def pin_message(
peer = (await self._resolve_to_packed(chat))._to_input_peer() self: Client, chat: Peer | PeerRef, /, message_id: int
) -> Message:
return self._build_message_map( return self._build_message_map(
await self( await self(
functions.messages.update_pinned_message( 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() ).get_single()
async def unpin_message( async def unpin_message(
self: Client, chat: ChatLike, message_id: int | Literal["all"] self: Client, chat: Peer | PeerRef, /, message_id: int | Literal["all"]
) -> None: ) -> None:
peer = (await self._resolve_to_packed(chat))._to_input_peer()
if message_id == "all": if message_id == "all":
await self( await self(
functions.messages.unpin_all_messages( functions.messages.unpin_all_messages(
peer=peer, peer=chat._ref._to_input_peer(),
top_msg_id=None, top_msg_id=None,
) )
) )
else: else:
await self( await self(
functions.messages.update_pinned_message( 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( async def read_message(
self: Client, chat: ChatLike, message_id: int | Literal["all"] self: Client, chat: Peer | PeerRef, /, message_id: int | Literal["all"]
) -> None: ) -> None:
packed = await self._resolve_to_packed(chat)
if message_id == "all": if message_id == "all":
message_id = 0 message_id = 0
if packed.is_channel(): peer = chat._ref
if isinstance(peer, ChannelRef):
await self( await self(
functions.channels.read_history( functions.channels.read_history(
channel=packed._to_input_channel(), max_id=message_id channel=peer._to_input_channel(), max_id=message_id
) )
) )
else: else:
await self( await self(
functions.messages.read_history( 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__( def __init__(
self, self,
client: Client, client: Client,
peer: Optional[abcs.InputPeer], peer: Optional[PeerRef],
random_id_to_id: dict[int, int], random_id_to_id: dict[int, int],
id_to_message: dict[int, Message], id_to_message: dict[int, Message],
) -> None: ) -> None:
@ -580,7 +577,9 @@ class MessageMap:
def _empty(self, id: int = 0) -> Message: def _empty(self, id: int = 0) -> Message:
return Message._from_raw( return Message._from_raw(
self._client, 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( def build_message_map(
client: Client, client: Client,
result: abcs.Updates, result: abcs.Updates,
peer: Optional[abcs.InputPeer], peer: Optional[PeerRef],
) -> MessageMap: ) -> MessageMap:
if isinstance(result, (types.Updates, types.UpdatesCombined)): if isinstance(result, (types.Updates, types.UpdatesCombined)):
updates = result.updates updates = result.updates

View File

@ -196,9 +196,7 @@ async def connect(self: Client) -> None:
self._session.user = SessionUser( self._session.user = SessionUser(
id=me.id, dc=self._sender.dc_id, bot=me.bot, username=me.username id=me.id, dc=self._sender.dc_id, bot=me.bot, username=me.username
) )
packed = me.pack() self._chat_hashes.set_self_user(me.id, me.bot)
assert packed is not None
self._chat_hashes.set_self_user(packed)
self._dispatcher = asyncio.create_task(dispatcher(self)) self._dispatcher = asyncio.create_task(dispatcher(self))

View File

@ -20,7 +20,7 @@ UPDATE_LIMIT_EXCEEDED_LOG_COOLDOWN = 300
def on( 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]]]: ) -> Callable[[Callable[[Event], Awaitable[Any]]], Callable[[Event], Awaitable[Any]]]:
def wrapper( def wrapper(
handler: Callable[[Event], Awaitable[Any]] handler: Callable[[Event], Awaitable[Any]]
@ -34,6 +34,7 @@ def on(
def add_event_handler( def add_event_handler(
self: Client, self: Client,
handler: Callable[[Event], Awaitable[Any]], handler: Callable[[Event], Awaitable[Any]],
/,
event_cls: Type[Event], event_cls: Type[Event],
filter: Optional[Filter] = None, filter: Optional[Filter] = None,
) -> None: ) -> None:
@ -41,7 +42,7 @@ def add_event_handler(
def remove_event_handler( def remove_event_handler(
self: Client, handler: Callable[[Event], Awaitable[Any]] self: Client, handler: Callable[[Event], Awaitable[Any]], /
) -> None: ) -> None:
for event_cls, handlers in tuple(self._handlers.items()): for event_cls, handlers in tuple(self._handlers.items()):
for i in reversed(range(len(handlers))): for i in reversed(range(len(handlers))):
@ -52,7 +53,7 @@ def remove_event_handler(
def get_handler_filter( def get_handler_filter(
self: Client, handler: Callable[[Event], Awaitable[Any]] self: Client, handler: Callable[[Event], Awaitable[Any]], /
) -> Optional[Filter]: ) -> Optional[Filter]:
for handlers in self._handlers.values(): for handlers in self._handlers.values():
for h, f in handlers: for h, f in handlers:
@ -64,6 +65,7 @@ def get_handler_filter(
def set_handler_filter( def set_handler_filter(
self: Client, self: Client,
handler: Callable[[Event], Awaitable[Any]], handler: Callable[[Event], Awaitable[Any]],
/,
filter: Optional[Filter] = None, filter: Optional[Filter] = None,
) -> None: ) -> None:
for handlers in self._handlers.values(): for handlers in self._handlers.values():

View File

@ -1,21 +1,11 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional, Sequence
from ...mtproto import RpcError from ...mtproto import RpcError
from ...session import PackedType, PeerRef from ...session import GroupRef, PeerRef, UserRef
from ...tl import abcs, functions, types from ...tl import abcs, functions, types
from ..types import ( from ..types import AsyncList, Peer, User, build_chat_map, expand_peer, peer_id
AsyncList,
Channel,
ChatLike,
Group,
Peer,
User,
build_chat_map,
expand_peer,
peer_id,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from .client import Client 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") 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( 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( return resolved_peer_to_chat(
self, await self(functions.contacts.resolve_username(username=username)) self, await self(functions.contacts.resolve_username(username=username))
) )
async def get_chats( async def resolve_peers(self: Client, peers: Sequence[Peer | PeerRef], /) -> list[Peer]:
self: Client, chats: list[ChatLike] | tuple[ChatLike, ...] refs: list[PeerRef] = []
) -> list[Peer]:
packed_chats: list[PeerRef] = []
input_users: list[types.InputUser] = [] input_users: list[types.InputUser] = []
input_chats: list[int] = [] input_chats: list[int] = []
input_channels: list[types.InputChannel] = [] input_channels: list[types.InputChannel] = []
for chat in chats: for peer in peers:
packed = await resolve_to_packed(self, chat) peer = peer._ref
packed_chats.append(packed) refs.append(peer)
if packed.is_user(): if isinstance(peer, UserRef):
input_users.append(packed._to_input_user()) input_users.append(peer._to_input_user())
elif packed.is_chat(): elif isinstance(peer, GroupRef):
input_chats.append(packed.id) input_chats.append(peer._to_input_chat())
else: else:
input_channels.append(packed._to_input_channel()) input_channels.append(peer._to_input_channel())
if input_users: if input_users:
ret_users = await self(functions.users.get_users(id=input_users)) ret_users = await self(functions.users.get_users(id=input_users))
@ -101,152 +89,18 @@ async def get_chats(
if input_chats: if input_chats:
ret_chats = await self(functions.messages.get_chats(id=input_chats)) ret_chats = await self(functions.messages.get_chats(id=input_chats))
assert isinstance(ret_chats, types.messages.Chats) assert isinstance(ret_chats, types.messages.Chats)
groups = list(ret_chats.chats) chats = list(ret_chats.chats)
else: else:
groups = [] chats = []
if input_channels: if input_channels:
ret_chats = await self(functions.channels.get_channels(id=input_channels)) ret_chats = await self(functions.channels.get_channels(id=input_channels))
assert isinstance(ret_chats, types.messages.Chats) assert isinstance(ret_chats, types.messages.Chats)
channels = list(ret_chats.chats) chats.extend(ret_chats.chats)
else:
channels = []
chat_map = build_chat_map(self, users, groups + channels) chat_map = build_chat_map(self, users, chats)
return [ return [
chat_map.get(chat.id) chat_map.get(ref.identifier)
or expand_peer(self, chat._to_peer(), broadcast=chat.ty == PackedType.BROADCAST) or expand_peer(self, ref._to_peer(), broadcast=None)
for chat in packed_chats 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")

View File

@ -149,7 +149,7 @@ class MessageRead(Event):
@property @property
def chat(self) -> Peer: def chat(self) -> Peer:
""" """
The :term:`chat` when the messages were read. The :term:`peer` where the messages were read.
""" """
peer = self._peer() peer = self._peer()
pid = peer_id(peer) pid = peer_id(peer)

View File

@ -2,6 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Self from typing import TYPE_CHECKING, Optional, Self
from ...session import PeerRef
from ...tl import abcs, functions, types from ...tl import abcs, functions, types
from ..client.messages import CherryPickedList from ..client.messages import CherryPickedList
from ..types import Message, Peer from ..types import Message, Peer
@ -79,9 +80,9 @@ class ButtonCallback(Event):
""" """
pid = peer_id(self._raw.peer) 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( lst._ids.append(
types.InputMessageCallbackQuery( types.InputMessageCallbackQuery(
id=self._raw.msg_id, query_id=self._raw.query_id id=self._raw.msg_id, query_id=self._raw.query_id

View File

@ -25,16 +25,7 @@ from .message import (
from .meta import NoPublicConstructor from .meta import NoPublicConstructor
from .participant import Participant from .participant import Participant
from .password_token import PasswordToken from .password_token import PasswordToken
from .peer import ( from .peer import Channel, Group, Peer, User, build_chat_map, expand_peer, peer_id
Channel,
ChatLike,
Group,
Peer,
User,
build_chat_map,
expand_peer,
peer_id,
)
from .recent_action import RecentAction from .recent_action import RecentAction
__all__ = [ __all__ = [
@ -45,7 +36,6 @@ __all__ = [
"CallbackAnswer", "CallbackAnswer",
"Channel", "Channel",
"Peer", "Peer",
"ChatLike",
"Group", "Group",
"User", "User",
"build_chat_map", "build_chat_map",

View File

@ -4,11 +4,12 @@ import mimetypes
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from ...session import PeerRef
from ...tl import abcs, functions, types from ...tl import abcs, functions, types
from .file import InFileLike, try_get_url_path from .file import InFileLike, try_get_url_path
from .message import Message, generate_random_id, parse_message from .message import Message, generate_random_id, parse_message
from .meta import NoPublicConstructor from .meta import NoPublicConstructor
from .peer import ChatLike from .peer import Peer
if TYPE_CHECKING: if TYPE_CHECKING:
from ..client.client import Client from ..client.client import Client
@ -197,7 +198,7 @@ class AlbumBuilder(metaclass=NoPublicConstructor):
) )
async def send( async def send(
self, chat: ChatLike, *, reply_to: Optional[int] = None self, peer: Peer | PeerRef, *, reply_to: Optional[int] = None
) -> list[Message]: ) -> list[Message]:
""" """
Send the album. Send the album.
@ -214,7 +215,6 @@ class AlbumBuilder(metaclass=NoPublicConstructor):
messages = await album.send(chat) messages = await album.send(chat)
""" """
peer = (await self._client._resolve_to_packed(chat))._to_input_peer()
msg_map = self._client._build_message_map( msg_map = self._client._build_message_map(
await self._client( await self._client(
functions.messages.send_multi_media( functions.messages.send_multi_media(
@ -223,7 +223,7 @@ class AlbumBuilder(metaclass=NoPublicConstructor):
clear_draft=False, clear_draft=False,
noforwards=False, noforwards=False,
update_stickersets_order=False, update_stickersets_order=False,
peer=peer, peer=peer._ref._to_input_peer(),
reply_to=( reply_to=(
types.InputReplyToMessage( types.InputReplyToMessage(
reply_to_msg_id=reply_to, top_msg_id=None reply_to_msg_id=reply_to, top_msg_id=None
@ -236,6 +236,6 @@ class AlbumBuilder(metaclass=NoPublicConstructor):
send_as=None, send_as=None,
) )
), ),
peer, peer._ref,
) )
return [msg_map.with_random_id(media.random_id) for media in self._medias] return [msg_map.with_random_id(media.random_id) for media in self._medias]

View File

@ -47,14 +47,12 @@ class Callback(InlineButton):
The bot's answer will be returned, or :data:`None` if they don't answer in time. The bot's answer will be returned, or :data:`None` if they don't answer in time.
""" """
message = self._message() message = self._message()
packed = message.chat.pack()
assert packed
return CallbackAnswer._create( return CallbackAnswer._create(
await message._client( await message._client(
functions.messages.get_bot_callback_answer( functions.messages.get_bot_callback_answer(
game=False, game=False,
peer=packed._to_input_peer(), peer=message.chat._ref._to_input_peer(),
msg_id=message.id, msg_id=message.id,
data=self.data, data=self.data,
password=None, password=None,

View File

@ -150,7 +150,7 @@ class Draft(metaclass=NoPublicConstructor):
new_draft = await old_draft.edit('new text', link_preview=False) new_draft = await old_draft.edit('new text', link_preview=False)
""" """
return await self._client.edit_draft( return await self._client.edit_draft(
await self._packed_chat(), self._peer_ref(),
text, text,
markdown=markdown, markdown=markdown,
html=html, html=html,
@ -158,13 +158,11 @@ class Draft(metaclass=NoPublicConstructor):
reply_to=reply_to, reply_to=reply_to,
) )
async def _packed_chat(self) -> PeerRef: def _peer_ref(self) -> PeerRef:
packed = None
if chat := self._chat_map.get(peer_id(self._peer)): if chat := self._chat_map.get(peer_id(self._peer)):
packed = chat.pack() return chat._ref
if packed is None: else:
packed = await self._client._resolve_to_packed(peer_id(self._peer)) return PeerRef._empty_from_peer(self._peer)
return packed
async def send(self) -> Message: async def send(self) -> Message:
""" """
@ -180,9 +178,6 @@ class Draft(metaclass=NoPublicConstructor):
await draft.send(clear=False) await draft.send(clear=False)
""" """
packed = await self._packed_chat()
peer = packed._to_input_peer()
reply_to = self.replied_message_id reply_to = self.replied_message_id
message = getattr(self._raw, "message", "") message = getattr(self._raw, "message", "")
entities = getattr(self._raw, "entities", None) entities = getattr(self._raw, "entities", None)
@ -196,7 +191,7 @@ class Draft(metaclass=NoPublicConstructor):
clear_draft=True, clear_draft=True,
noforwards=False, noforwards=False,
update_stickersets_order=False, update_stickersets_order=False,
peer=peer, peer=self._peer_ref()._to_input_peer(),
reply_to=( reply_to=(
types.InputReplyToMessage(reply_to_msg_id=reply_to, top_msg_id=None) types.InputReplyToMessage(reply_to_msg_id=reply_to, top_msg_id=None)
if reply_to if reply_to
@ -221,7 +216,7 @@ class Draft(metaclass=NoPublicConstructor):
if self._client._session.user if self._client._session.user
else None else None
), ),
peer_id=packed._to_peer(), peer_id=self._peer_ref()._to_peer(),
reply_to=( reply_to=(
types.MessageReplyHeader( types.MessageReplyHeader(
reply_to_scheduled=False, reply_to_scheduled=False,
@ -240,9 +235,9 @@ class Draft(metaclass=NoPublicConstructor):
ttl_period=result.ttl_period, ttl_period=result.ttl_period,
) )
else: else:
return self._client._build_message_map(result, peer).with_random_id( return self._client._build_message_map(
random_id result, self._peer_ref()
) ).with_random_id(random_id)
async def delete(self) -> None: async def delete(self) -> None:
""" """

View File

@ -2,10 +2,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional 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 .message import Message, generate_random_id
from .meta import NoPublicConstructor from .meta import NoPublicConstructor
from .peer import ChatLike from .peer import Peer
if TYPE_CHECKING: if TYPE_CHECKING:
from ..client.client import Client from ..client.client import Client
@ -23,7 +24,7 @@ class InlineResult(metaclass=NoPublicConstructor):
client: Client, client: Client,
results: types.messages.BotResults, results: types.messages.BotResults,
result: types.BotInlineMediaResult | types.BotInlineResult, result: types.BotInlineMediaResult | types.BotInlineResult,
default_peer: abcs.InputPeer, default_peer: Optional[PeerRef],
): ):
self._client = client self._client = client
self._raw_results = results self._raw_results = results
@ -50,7 +51,7 @@ class InlineResult(metaclass=NoPublicConstructor):
async def send( async def send(
self, self,
chat: Optional[ChatLike] = None, peer: Optional[Peer | PeerRef] = None,
) -> Message: ) -> Message:
""" """
Send the result to the desired chat. Send the result to the desired chat.
@ -62,13 +63,12 @@ class InlineResult(metaclass=NoPublicConstructor):
:return: The sent message. :return: The sent message.
""" """
if chat is None and isinstance(self._default_peer, types.InputPeerEmpty): if peer is None:
if self._default_peer is None:
raise ValueError("no target chat was specified") raise ValueError("no target chat was specified")
if chat is not None:
peer = (await self._client._resolve_to_packed(chat))._to_input_peer()
else:
peer = self._default_peer peer = self._default_peer
else:
peer = peer._ref
random_id = generate_random_id() random_id = generate_random_id()
return self._client._build_message_map( return self._client._build_message_map(
@ -78,7 +78,7 @@ class InlineResult(metaclass=NoPublicConstructor):
background=False, background=False,
clear_draft=False, clear_draft=False,
hide_via=False, hide_via=False,
peer=peer, peer=peer._to_input_peer(),
reply_to=None, reply_to=None,
random_id=random_id, random_id=random_id,
query_id=self._raw_results.query_id, query_id=self._raw_results.query_id,

View File

@ -4,6 +4,7 @@ import datetime
import time import time
from typing import TYPE_CHECKING, Any, Optional, Self, Sequence from typing import TYPE_CHECKING, Any, Optional, Self, Sequence
from ...session import PeerRef
from ...tl import abcs, types from ...tl import abcs, types
from ..parsers import ( from ..parsers import (
generate_html_message, generate_html_message,
@ -14,7 +15,7 @@ from ..parsers import (
from .buttons import Button, as_concrete_row, create_button from .buttons import Button, as_concrete_row, create_button
from .file import File from .file import File
from .meta import NoPublicConstructor from .meta import NoPublicConstructor
from .peer import ChatLike, Peer, expand_peer, peer_id from .peer import Peer, expand_peer, peer_id
if TYPE_CHECKING: if TYPE_CHECKING:
from ..client.client import Client from ..client.client import Client
@ -186,7 +187,7 @@ class Message(metaclass=NoPublicConstructor):
@property @property
def chat(self) -> Peer: 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) peer = self._raw.peer_id or types.PeerUser(user_id=0)
pid = peer_id(peer) pid = peer_id(peer)
@ -199,7 +200,7 @@ class Message(metaclass=NoPublicConstructor):
@property @property
def sender(self) -> Optional[Peer]: 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`. 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: if self.replied_message_id is not None:
from ..client.messages import CherryPickedList 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)) lst._ids.append(types.InputMessageReplyTo(id=self.id))
return (await lst)[0] return (await lst)[0]
return None return None
@ -415,7 +416,7 @@ class Message(metaclass=NoPublicConstructor):
buttons=buttons, buttons=buttons,
) )
async def forward(self, target: ChatLike) -> Message: async def forward(self, target: Peer | PeerRef) -> Message:
""" """
Alias for :meth:`telethon.Client.forward_messages`. Alias for :meth:`telethon.Client.forward_messages`.

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import datetime import datetime
from typing import TYPE_CHECKING, Optional, Self, Sequence from typing import TYPE_CHECKING, Optional, Self, Sequence
from ...session import PeerRef from ...session import ChannelRef, GroupRef
from ...tl import abcs, types from ...tl import abcs, types
from .admin_right import AdminRight from .admin_right import AdminRight
from .chat_restriction import ChatRestriction from .chat_restriction import ChatRestriction
@ -24,7 +24,7 @@ class Participant(metaclass=NoPublicConstructor):
def __init__( def __init__(
self, self,
client: Client, client: Client,
chat: PeerRef, chat: GroupRef | ChannelRef,
participant: ( participant: (
types.ChannelParticipant types.ChannelParticipant
| types.ChannelParticipantSelf | types.ChannelParticipantSelf
@ -47,7 +47,7 @@ class Participant(metaclass=NoPublicConstructor):
def _from_raw_channel( def _from_raw_channel(
cls, cls,
client: Client, client: Client,
chat: PeerRef, chat: ChannelRef,
participant: abcs.ChannelParticipant, participant: abcs.ChannelParticipant,
chat_map: dict[int, Peer], chat_map: dict[int, Peer],
) -> Self: ) -> Self:
@ -70,7 +70,7 @@ class Participant(metaclass=NoPublicConstructor):
def _from_raw_chat( def _from_raw_chat(
cls, cls,
client: Client, client: Client,
chat: PeerRef, chat: GroupRef,
participant: abcs.ChatParticipant, participant: abcs.ChatParticipant,
chat_map: dict[int, Peer], chat_map: dict[int, Peer],
) -> Self: ) -> Self:
@ -193,7 +193,14 @@ class Participant(metaclass=NoPublicConstructor):
""" """
participant = self.user or self.banned or self.left participant = self.user or self.banned or self.left
assert participant 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( async def set_restrictions(
self, self,

View File

@ -5,7 +5,6 @@ import sys
from collections import defaultdict from collections import defaultdict
from typing import TYPE_CHECKING, Optional, Sequence from typing import TYPE_CHECKING, Optional, Sequence
from ....session import PeerRef
from ....tl import abcs, types from ....tl import abcs, types
from .channel import Channel from .channel import Channel
from .group import Group from .group import Group
@ -15,8 +14,6 @@ from .user import User
if TYPE_CHECKING: if TYPE_CHECKING:
from ...client.client import Client from ...client.client import Client
ChatLike = Peer | PeerRef | int | str
def build_chat_map( def build_chat_map(
client: Client, users: Sequence[abcs.User], chats: Sequence[abcs.Chat] client: Client, users: Sequence[abcs.User], chats: Sequence[abcs.Chat]

View File

@ -1,6 +1,6 @@
from typing import Optional, Self from typing import Optional, Self
from ....session import PackedType, PeerRef from ....session import ChannelRef
from ....tl import abcs, types from ....tl import abcs, types
from ..meta import NoPublicConstructor from ..meta import NoPublicConstructor
from .peer import Peer from .peer import Peer
@ -50,18 +50,12 @@ class Channel(Peer, metaclass=NoPublicConstructor):
def username(self) -> Optional[str]: def username(self) -> Optional[str]:
return getattr(self._raw, "username", None) return getattr(self._raw, "username", None)
def pack(self) -> Optional[PeerRef]: @property
if self._raw.access_hash is None: def ref(self) -> ChannelRef:
return None return ChannelRef(self._raw.id, self._raw.access_hash)
else:
return PeerRef( @property
ty=( def _ref(self) -> ChannelRef:
PackedType.GIGAGROUP return self.ref
if getattr(self._raw, "gigagroup", False)
else PackedType.BROADCAST
),
id=self._raw.id,
access_hash=self._raw.access_hash,
)
# endregion Overrides # endregion Overrides

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import datetime import datetime
from typing import TYPE_CHECKING, Optional, Self, Sequence from typing import TYPE_CHECKING, Optional, Self, Sequence
from ....session import PackedType, PeerRef from ....session import ChannelRef, GroupRef
from ....tl import abcs, types from ....tl import abcs, types
from ..chat_restriction import ChatRestriction from ..chat_restriction import ChatRestriction
from ..meta import NoPublicConstructor from ..meta import NoPublicConstructor
@ -65,17 +65,16 @@ class Group(Peer, metaclass=NoPublicConstructor):
def username(self) -> Optional[str]: def username(self) -> Optional[str]:
return getattr(self._raw, "username", None) 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)): if isinstance(self._raw, (types.ChatEmpty, types.Chat, types.ChatForbidden)):
return PeerRef(ty=PackedType.CHAT, id=self._raw.id, access_hash=None) return GroupRef(self._raw.id, None)
elif self._raw.access_hash is None:
return None
else: else:
return PeerRef( return ChannelRef(self._raw.id, self._raw.access_hash)
ty=PackedType.MEGAGROUP,
id=self._raw.id, @property
access_hash=self._raw.access_hash, def _ref(self) -> GroupRef | ChannelRef:
) return self.ref
# endregion Overrides # endregion Overrides

View File

@ -1,7 +1,7 @@
import abc import abc
from typing import Optional from typing import Optional
from ....session import PeerRef from ....session import ChannelRef, GroupRef, UserRef
class Peer(abc.ABC): class Peer(abc.ABC):
@ -15,7 +15,7 @@ class Peer(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
def id(self) -> int: def id(self) -> int:
""" """
The chat's integer identifier. The peer's integer identifier.
This identifier is always a positive number. This identifier is always a positive number.
@ -39,20 +39,28 @@ class Peer(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
def username(self) -> Optional[str]: 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 ``@``. The returned string will *not* contain the at-sign ``@``.
""" """
@property
@abc.abstractmethod @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. This can be used to persist the reference to a database or disk,
Unlike resolving usernames, packed chats can be reused without costly calls. or to create inline mentions.
.. seealso:: .. 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

View File

@ -1,6 +1,6 @@
from typing import Optional, Self from typing import Optional, Self
from ....session import PackedType, PeerRef from ....session import UserRef
from ....tl import abcs, types from ....tl import abcs, types
from ..meta import NoPublicConstructor from ..meta import NoPublicConstructor
from .peer import Peer from .peer import Peer
@ -87,15 +87,13 @@ class User(Peer, metaclass=NoPublicConstructor):
def username(self) -> Optional[str]: def username(self) -> Optional[str]:
return self._raw.username return self._raw.username
def pack(self) -> Optional[PeerRef]: @property
if self._raw.access_hash is None: def ref(self) -> UserRef:
return None return UserRef(self._raw.id, self._raw.access_hash)
else:
return PeerRef( @property
ty=PackedType.BOT if self._raw.bot else PackedType.USER, def _ref(self) -> UserRef:
id=self._raw.id, return self.ref
access_hash=self._raw.access_hash,
)
# endregion Overrides # endregion Overrides

View File

@ -1,4 +1,12 @@
from .chat import ChatHashCache, PackedType, PeerRef from .chat import (
ChannelRef,
ChatHashCache,
GroupRef,
PeerAuth,
PeerIdentifier,
PeerRef,
UserRef,
)
from .message_box import ( from .message_box import (
BOT_CHANNEL_DIFF_LIMIT, BOT_CHANNEL_DIFF_LIMIT,
NO_UPDATES_TIMEOUT, NO_UPDATES_TIMEOUT,
@ -14,9 +22,13 @@ from .session import ChannelState, DataCenter, Session, UpdateState, User
from .storage import MemorySession, SqliteSession, Storage from .storage import MemorySession, SqliteSession, Storage
__all__ = [ __all__ = [
"ChannelRef",
"ChatHashCache", "ChatHashCache",
"GroupRef",
"PeerAuth",
"PeerIdentifier",
"PeerRef", "PeerRef",
"PackedType", "UserRef",
"BOT_CHANNEL_DIFF_LIMIT", "BOT_CHANNEL_DIFF_LIMIT",
"NO_UPDATES_TIMEOUT", "NO_UPDATES_TIMEOUT",
"USER_CHANNEL_DIFF_LIMIT", "USER_CHANNEL_DIFF_LIMIT",

View File

@ -1,4 +1,12 @@
from .hash_cache import ChatHashCache 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",
]

View File

@ -1,14 +1,16 @@
from typing import Any, Optional, Sequence from typing import Any, Optional, Sequence, Type, TypeAlias
from ...tl import abcs, types 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: class ChatHashCache:
__slots__ = ("_hash_map", "_self_id", "_self_bot") __slots__ = ("_hash_map", "_self_id", "_self_bot")
def __init__(self, self_user: Optional[tuple[int, bool]]): 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_id = self_user[0] if self_user else None
self._self_bot = self_user[1] if self_user else False self._self_bot = self_user[1] if self_user else False
@ -21,15 +23,14 @@ class ChatHashCache:
def is_self_bot(self) -> bool: def is_self_bot(self) -> bool:
return self._self_bot return self._self_bot
def set_self_user(self, user: PeerRef) -> None: def set_self_user(self, identifier: int, bot: bool) -> None:
assert user.ty in (PackedType.USER, PackedType.BOT) self._self_id = identifier
self._self_bot = user.ty == PackedType.BOT self._self_bot = bot
self._self_id = user.id
def get(self, id: int) -> Optional[PeerRef]: def get(self, identifier: int) -> Optional[PeerRef]:
if (entry := self._hash_map.get(id)) is not None: if (entry := self._hash_map.get(identifier)) is not None:
hash, ty = entry cls, authorization = entry
return PeerRef(ty, id, hash) return cls(identifier, authorization)
else: else:
return None return None
@ -38,8 +39,8 @@ class ChatHashCache:
self._self_id = None self._self_id = None
self._self_bot = False self._self_bot = False
def _has(self, id: int) -> bool: def _has(self, identifier: int) -> bool:
return id in self._hash_map return identifier in self._hash_map
def _has_peer(self, peer: abcs.Peer) -> bool: def _has_peer(self, peer: abcs.Peer) -> bool:
if isinstance(peer, types.PeerUser): if isinstance(peer, types.PeerUser):
@ -140,8 +141,7 @@ class ChatHashCache:
pass pass
elif isinstance(user, types.User): elif isinstance(user, types.User):
if not user.min and user.access_hash is not None: 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] = (UserRef, user.access_hash)
self._hash_map[user.id] = (user.access_hash, ty)
else: else:
success &= user.id in self._hash_map success &= user.id in self._hash_map
else: else:
@ -152,18 +152,11 @@ class ChatHashCache:
pass pass
elif isinstance(chat, types.Channel): elif isinstance(chat, types.Channel):
if not chat.min and chat.access_hash is not None: if not chat.min and chat.access_hash is not None:
if chat.megagroup: self._hash_map[chat.id] = (ChannelRef, chat.access_hash)
ty = PackedType.MEGAGROUP
elif chat.gigagroup:
ty = PackedType.GIGAGROUP
else:
ty = PackedType.BROADCAST
self._hash_map[chat.id] = (chat.access_hash, ty)
else: else:
success &= chat.id in self._hash_map success &= chat.id in self._hash_map
elif isinstance(chat, types.ChannelForbidden): elif isinstance(chat, types.ChannelForbidden):
ty = PackedType.MEGAGROUP if chat.megagroup else PackedType.BROADCAST self._hash_map[chat.id] = (ChannelRef, chat.access_hash)
self._hash_map[chat.id] = (chat.access_hash, ty)
else: else:
raise RuntimeError("unexpected case") raise RuntimeError("unexpected case")

View File

@ -1,150 +1,254 @@
from __future__ import annotations
import abc
import base64
import re
import struct import struct
from enum import IntFlag from typing import Optional, Self, TypeAlias
from typing import Optional, Self
from ...tl import abcs, types from ...tl import abcs, types
PeerIdentifier: TypeAlias = int
PeerAuth: TypeAlias = Optional[int]
class PackedType(IntFlag): USER_PREFIX = "u."
""" GROUP_PREFIX = "g."
The type of a :class:`PeerRef`. CHANNEL_PREFIX = "c."
class PeerRef(abc.ABC):
""" """
A reference to a :term:`peer`.
# bits: zero, has-access-hash, channel, broadcast, group, chat, user, bot References can be used to interact with any method that expects a peer,
USER = 0b0000_0010 without the need to fetch or resolve the entire peer beforehand.
BOT = 0b0000_0011
CHAT = 0b0000_0100
MEGAGROUP = 0b0010_1000
BROADCAST = 0b0011_0000
GIGAGROUP = 0b0011_1000
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: You can access the :attr:`telethon.types.Peer.ref` attribute on :class:`~telethon.types.User`,
"""
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`,
:class:`~telethon.types.Group` or :class:`~telethon.types.Channel` to obtain it. :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:: .. 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: def __init__(
self.ty = ty self, identifier: PeerIdentifier, authorization: PeerAuth = None
self.id = id ) -> None:
self.access_hash = access_hash assert (
identifier >= 0
def __bytes__(self) -> bytes: ), "PeerRef identifiers must be positive; see the documentation for Peers"
return struct.pack( self.identifier = identifier
"<Bqq", self.authorization = authorization
self.ty.value | (0 if self.access_hash is None else 0b0100_0000),
self.id,
self.access_hash or 0,
)
@classmethod @classmethod
def from_bytes(cls, data: bytes) -> Self: def from_str(cls, string: str, /) -> UserRef | GroupRef | ChannelRef:
ty_byte, id, access_hash = struct.unpack("<Bqq", data)
has_hash = (ty_byte & 0b0100_0000) != 0
ty = PackedType(ty_byte & 0b0011_1111)
return cls(ty, id, access_hash if has_hash else None)
@property
def hex(self) -> str:
""" """
Convenience property to convert to bytes and represent them as hexadecimal numbers: 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 @classmethod
def from_hex(cls, hex: str) -> Self: def _empty_from_peer(cls, peer: abcs.Peer) -> UserRef | GroupRef | ChannelRef:
""" if isinstance(peer, types.PeerUser):
Convenience method to convert hexadecimal numbers into bytes then passed to :meth:`from_bytes`: return UserRef(peer.user_id, None)
elif isinstance(peer, types.PeerChat):
:param hex: return GroupRef(peer.chat_id, None)
Hexadecimal numbers to convert from. elif isinstance(peer, types.PeerChannel):
return ChannelRef(peer.channel_id, None)
.. code-block:: else:
raise RuntimeError("unexpected case")
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,
)
@abc.abstractmethod
def _to_peer(self) -> abcs.Peer: def _to_peer(self) -> abcs.Peer:
if self.is_user(): pass
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")
@abc.abstractmethod
def _to_input_peer(self) -> abcs.InputPeer: def _to_input_peer(self) -> abcs.InputPeer:
if self.is_user(): pass
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")
def _to_input_user(self) -> types.InputUser: @abc.abstractmethod
if self.is_user(): def __str__(self) -> str:
return types.InputUser(user_id=self.id, access_hash=self.access_hash or 0) """
else: Format the reference into a :class:`str`.
raise TypeError("chat is not a user")
def _to_chat_id(self) -> int: .. seealso::
if self.is_chat():
return self.id
else:
raise TypeError("chat is not a group")
def _to_input_channel(self) -> types.InputChannel: :doc:`/concepts/messages`, to learn how this can be used to format inline mentions in messages.
if self.is_channel(): """
return types.InputChannel(
channel_id=self.id, access_hash=self.access_hash or 0 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: def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__): if not isinstance(other, self.__class__):
return NotImplemented return NotImplemented
return ( return (
self.ty == other.ty self.identifier == other.identifier
and self.id == other.id and self.authorization == other.authorization
and self.access_hash == other.access_hash )
@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: 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

View File

@ -546,12 +546,12 @@ class MessageBox:
else: else:
return None return None
packed = chat_hashes.get(id) ref = chat_hashes.get(id)
if packed: if ref:
assert packed.access_hash is not None assert ref.authorization is not None
channel = types.InputChannel( channel = types.InputChannel(
channel_id=packed.id, channel_id=ref.identifier,
access_hash=packed.access_hash, access_hash=ref.authorization,
) )
if state := self.map.get(entry): if state := self.map.get(entry):
gd = functions.updates.get_channel_difference( gd = functions.updates.get_channel_difference(

View File

@ -23,7 +23,7 @@ from .._impl.client.types import (
User, User,
) )
from .._impl.client.types.buttons import Button, InlineButton from .._impl.client.types.buttons import Button, InlineButton
from .._impl.session import PackedType, PeerRef from .._impl.session import ChannelRef, GroupRef, PeerRef, UserRef
__all__ = [ __all__ = [
"AdminRight", "AdminRight",
@ -46,6 +46,8 @@ __all__ = [
"User", "User",
"Button", "Button",
"InlineButton", "InlineButton",
"ChannelRef",
"GroupRef",
"PeerRef", "PeerRef",
"PackedType", "UserRef",
] ]

View File

@ -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

View File

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

View File

@ -28,7 +28,7 @@ class FunctionMethodsVisitor(ast.NodeVisitor):
self._try_add_def(node) self._try_add_def(node)
def _try_add_def(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: 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")), *_]: case [ast.arg(arg="self", annotation=ast.Name(id="Client")), *_]:
self.methods.append(node) self.methods.append(node)
case _: case _:
@ -94,13 +94,19 @@ def main() -> None:
call: ast.AST = ast.Call( call: ast.AST = ast.Call(
func=ast.Name(id=function.name, ctx=ast.Load()), 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=[ keywords=[
ast.keyword(arg=a.arg, value=ast.Name(id=a.arg, ctx=ast.Load())) ast.keyword(arg=a.arg, value=ast.Name(id=a.arg, ctx=ast.Load()))
for a in function.args.kwonlyargs for a in function.args.kwonlyargs
], ],
) )
if function.args.posonlyargs:
function.args.posonlyargs[0].annotation = None
else:
function.args.args[0].annotation = None function.args.args[0].annotation = None
if isinstance(function, ast.AsyncFunctionDef): if isinstance(function, ast.AsyncFunctionDef):