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::
:sorted:
chat
peer
A :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`.
.. seealso:: The :doc:`../concepts/chats` concept.
.. seealso:: The :doc:`../concepts/peers` concept.
yourself
The logged-in account, whether that represents a bot or a user with a phone number.

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:
* ``tg://user?id=ab1234cd6789`` for inline mentions.
To make sure the mention works, use :attr:`types.PeerRef.hex`.
You can also use :attr:`types.User.id`, but the mention will fail if the user is not in cache.
* ``tg://user?ref=u.123.A4B5`` for inline mentions.
You can obtain the reference using :attr:`types.Peer.ref` (as in ``f'tg://user?ref={user.ref}'``).
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.
You must use the document identifier as the value.
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
The term :term:`chat` is extremely overloaded, so it's no surprise many are confused by what it means.
This section should hopefully clear that up.
The term :term:`peer` may sound strange at first, but it's the best we have after much consideration.
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.
Therefore, a Telethon :term:`chat` can be another user, a bot, a group, or a broadcast channel.
All of those are places where messages can be sent.
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:`peer`" represents an entity with various attributes: identifier, username, photo, title, and other information depending on its type.
Of course, chats do more things than contain messages.
They often have a name, username, photo, description, and other information.
The :class:`~types.PeerRef` type represents a reference to a :class:`~types.Peer`, and can be obtained from its :attr:`~types.Peer.ref` attribute.
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,
it means that it will be either a :class:`~types.User`, :class:`~types.Group` or :class:`~types.Channel`.
| Most methods accept either the :class:`~types.Peer` or :class:`~types.PeerRef` (and their subclasses) as input.
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`.
The following types are chat-like:
A Telethon "chat" refers to either groups and channels, or the place where messages are sent to.
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").
* An ``'@username'``. The at-sign ``@`` is optional. Note that links are not supported.
* 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`.
A Telethon "group" is used to refer to either small group chats or supergroups.
This matches what the interface of official applications call these entities.
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".
You only need to know about this if you plan to use the :term:`Raw API`.
.. note::
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`.
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.
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.
* Chat IDs are negative.
* Channel IDs are *also* negative, but are prefixed by ``-100``.
Telethon encourages the use of :class:`~types.PeerRef` instead of naked identifiers.
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:
Telethon does not support Bot API's formatted identifiers, and instead expects you to create the appropriated :class:`~types.PeerRef`:
.. code-block:: python
# If -1001234 is your ID...
from telethon.types import PackedChat, PackedType
chat = PackedChat(PackedType.BROADCAST, 1234, None)
# ...you need to explicitly create a PackedChat with id=1234 and set the corresponding type (a channel).
# The access hash (see below) will be None, which may or may not work.
from telethon.types import UserRef, GroupRef, ChannelRef
user = UserRef(123) # user_id 123 from bot API becomes 123
group = GroupRef(456) # chat_id -456 from bot API becomes 456
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:
* …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`.
Chats access hash
-----------------
Access hashes and authorizations
--------------------------------
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.
This compact type can be used anywhere a chat is expected.
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 peer is expected.
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.
The same is true for user accounts, although to a lesser extent.
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.
When you create a :class:`~types.PeerRef` without specifying an authorization, a bogus :term:`access hash` will be used.

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.
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.
The concept of "marked IDs" also no longer exists.
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.
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.
.. 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.

View File

@ -78,13 +78,13 @@ Concepts
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::
:hidden:
:caption: Concepts
concepts/chats
concepts/peers
concepts/updates
concepts/messages
concepts/sessions

View File

@ -118,6 +118,16 @@ Private definitions
New-type wrapper around :class:`int` used as a message identifier.
.. currentmodule:: telethon._impl.session.chat.peer_ref
.. class:: PeerIdentifier
New-type wrapper around :class:`int` used as a message identifier.
.. class:: PeerAuth
New-type wrapper around :class:`int` used as a message identifier.
.. currentmodule:: telethon._impl.mtsender.sender
.. autoclass:: AsyncReader

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,11 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Sequence
from ...mtproto import RpcError
from ...session import PackedType, PeerRef
from ...session import GroupRef, PeerRef, UserRef
from ...tl import abcs, functions, types
from ..types import (
AsyncList,
Channel,
ChatLike,
Group,
Peer,
User,
build_chat_map,
expand_peer,
peer_id,
)
from ..types import AsyncList, Peer, User, build_chat_map, expand_peer, peer_id
if TYPE_CHECKING:
from .client import Client
@ -62,35 +52,33 @@ def resolved_peer_to_chat(client: Client, resolved: abcs.contacts.ResolvedPeer)
raise ValueError("no matching chat found in response")
async def resolve_phone(client: Client, phone: str) -> Peer:
async def resolve_phone(self: Client, phone: str, /) -> Peer:
return resolved_peer_to_chat(
client, await client(functions.contacts.resolve_phone(phone=phone))
self, await self(functions.contacts.resolve_phone(phone=phone))
)
async def resolve_username(self: Client, username: str) -> Peer:
async def resolve_username(self: Client, username: str, /) -> Peer:
return resolved_peer_to_chat(
self, await self(functions.contacts.resolve_username(username=username))
)
async def get_chats(
self: Client, chats: list[ChatLike] | tuple[ChatLike, ...]
) -> list[Peer]:
packed_chats: list[PeerRef] = []
async def resolve_peers(self: Client, peers: Sequence[Peer | PeerRef], /) -> list[Peer]:
refs: list[PeerRef] = []
input_users: list[types.InputUser] = []
input_chats: list[int] = []
input_channels: list[types.InputChannel] = []
for chat in chats:
packed = await resolve_to_packed(self, chat)
packed_chats.append(packed)
if packed.is_user():
input_users.append(packed._to_input_user())
elif packed.is_chat():
input_chats.append(packed.id)
for peer in peers:
peer = peer._ref
refs.append(peer)
if isinstance(peer, UserRef):
input_users.append(peer._to_input_user())
elif isinstance(peer, GroupRef):
input_chats.append(peer._to_input_chat())
else:
input_channels.append(packed._to_input_channel())
input_channels.append(peer._to_input_channel())
if input_users:
ret_users = await self(functions.users.get_users(id=input_users))
@ -101,152 +89,18 @@ async def get_chats(
if input_chats:
ret_chats = await self(functions.messages.get_chats(id=input_chats))
assert isinstance(ret_chats, types.messages.Chats)
groups = list(ret_chats.chats)
chats = list(ret_chats.chats)
else:
groups = []
chats = []
if input_channels:
ret_chats = await self(functions.channels.get_channels(id=input_channels))
assert isinstance(ret_chats, types.messages.Chats)
channels = list(ret_chats.chats)
else:
channels = []
chats.extend(ret_chats.chats)
chat_map = build_chat_map(self, users, groups + channels)
chat_map = build_chat_map(self, users, chats)
return [
chat_map.get(chat.id)
or expand_peer(self, chat._to_peer(), broadcast=chat.ty == PackedType.BROADCAST)
for chat in packed_chats
chat_map.get(ref.identifier)
or expand_peer(self, ref._to_peer(), broadcast=None)
for ref in refs
]
async def resolve_to_packed(
client: Client, chat: ChatLike | abcs.InputPeer | abcs.Peer
) -> PeerRef:
if isinstance(chat, PeerRef):
return chat
if isinstance(chat, (User, Group, Channel)):
packed = chat.pack() or client._chat_hashes.get(chat.id)
if packed is not None:
return packed
# Try anyway (may work for contacts or bot users).
if isinstance(chat, User):
ty = PackedType.USER
elif isinstance(chat, Group):
ty = PackedType.MEGAGROUP if chat.is_megagroup else PackedType.CHAT
else:
ty = PackedType.BROADCAST
return PeerRef(ty=ty, id=chat.id, access_hash=0)
if isinstance(chat, abcs.InputPeer):
if isinstance(chat, types.InputPeerEmpty):
raise ValueError("Cannot resolve chat")
elif isinstance(chat, types.InputPeerSelf):
if not client._session.user:
raise ValueError("Cannot resolve chat")
return PeerRef(
ty=PackedType.BOT if client._session.user.bot else PackedType.USER,
id=client._chat_hashes.self_id,
access_hash=0,
)
elif isinstance(chat, types.InputPeerChat):
return PeerRef(
ty=PackedType.CHAT,
id=chat.chat_id,
access_hash=None,
)
elif isinstance(chat, types.InputPeerUser):
return PeerRef(
ty=PackedType.USER,
id=chat.user_id,
access_hash=chat.access_hash,
)
elif isinstance(chat, types.InputPeerChannel):
return PeerRef(
ty=PackedType.BROADCAST,
id=chat.channel_id,
access_hash=chat.access_hash,
)
elif isinstance(chat, types.InputPeerUserFromMessage):
raise ValueError("Cannot resolve chat")
elif isinstance(chat, types.InputPeerChannelFromMessage):
raise ValueError("Cannot resolve chat")
else:
raise RuntimeError("unexpected case")
if isinstance(chat, abcs.Peer):
packed = client._chat_hashes.get(peer_id(chat))
if packed is not None:
return packed
if isinstance(chat, types.PeerUser):
return PeerRef(
ty=PackedType.USER,
id=chat.user_id,
access_hash=0,
)
elif isinstance(chat, types.PeerChat):
return PeerRef(
ty=PackedType.CHAT,
id=chat.chat_id,
access_hash=0,
)
elif isinstance(chat, types.PeerChannel):
return PeerRef(
ty=PackedType.BROADCAST,
id=chat.channel_id,
access_hash=0,
)
else:
raise RuntimeError("unexpected case")
if isinstance(chat, str):
if chat.startswith("+"):
resolved = await resolve_phone(client, chat)
elif chat == "me":
if me := client._session.user:
return PeerRef(
ty=PackedType.BOT if me.bot else PackedType.USER,
id=me.id,
access_hash=0,
)
else:
resolved = None
else:
resolved = await resolve_username(client, username=chat)
if resolved and (packed := resolved.pack()) is not None:
return packed
if isinstance(chat, int):
packed = client._chat_hashes.get(chat)
if packed is None:
raise ValueError("Cannot resolve chat")
return packed
raise ValueError("Cannot resolve chat")
def input_to_peer(
client: Client, input: Optional[abcs.InputPeer]
) -> Optional[abcs.Peer]:
if input is None:
return None
elif isinstance(input, types.InputPeerEmpty):
return None
elif isinstance(input, types.InputPeerSelf):
return types.PeerUser(user_id=client._chat_hashes.self_id)
elif isinstance(input, types.InputPeerChat):
return types.PeerChat(chat_id=input.chat_id)
elif isinstance(input, types.InputPeerUser):
return types.PeerUser(user_id=input.user_id)
elif isinstance(input, types.InputPeerChannel):
return types.PeerChannel(channel_id=input.channel_id)
elif isinstance(input, types.InputPeerUserFromMessage):
return None
elif isinstance(input, types.InputPeerChannelFromMessage):
return None
else:
raise RuntimeError("unexpected case")

View File

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

View File

@ -2,6 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Self
from ...session import PeerRef
from ...tl import abcs, functions, types
from ..client.messages import CherryPickedList
from ..types import Message, Peer
@ -79,9 +80,9 @@ class ButtonCallback(Event):
"""
pid = peer_id(self._raw.peer)
chat = self._chat_map.get(pid) or await self._client._resolve_to_packed(pid)
chat = self._chat_map.get(pid) or PeerRef._empty_from_peer(self._raw.peer)
lst = CherryPickedList(self._client, chat, [])
lst = CherryPickedList(self._client, chat._ref, [])
lst._ids.append(
types.InputMessageCallbackQuery(
id=self._raw.msg_id, query_id=self._raw.query_id

View File

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

View File

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

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.
"""
message = self._message()
packed = message.chat.pack()
assert packed
return CallbackAnswer._create(
await message._client(
functions.messages.get_bot_callback_answer(
game=False,
peer=packed._to_input_peer(),
peer=message.chat._ref._to_input_peer(),
msg_id=message.id,
data=self.data,
password=None,

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import datetime
import time
from typing import TYPE_CHECKING, Any, Optional, Self, Sequence
from ...session import PeerRef
from ...tl import abcs, types
from ..parsers import (
generate_html_message,
@ -14,7 +15,7 @@ from ..parsers import (
from .buttons import Button, as_concrete_row, create_button
from .file import File
from .meta import NoPublicConstructor
from .peer import ChatLike, Peer, expand_peer, peer_id
from .peer import Peer, expand_peer, peer_id
if TYPE_CHECKING:
from ..client.client import Client
@ -186,7 +187,7 @@ class Message(metaclass=NoPublicConstructor):
@property
def chat(self) -> Peer:
"""
The :term:`chat` when the message was sent.
The :term:`peer` where the message was sent.
"""
peer = self._raw.peer_id or types.PeerUser(user_id=0)
pid = peer_id(peer)
@ -199,7 +200,7 @@ class Message(metaclass=NoPublicConstructor):
@property
def sender(self) -> Optional[Peer]:
"""
The :term:`chat` that sent the message.
The :term:`peer` that sent the message.
This will usually be a :class:`User`, but can also be a :class:`Channel`.
@ -320,7 +321,7 @@ class Message(metaclass=NoPublicConstructor):
if self.replied_message_id is not None:
from ..client.messages import CherryPickedList
lst = CherryPickedList(self._client, self.chat, [])
lst = CherryPickedList(self._client, self.chat._ref, [])
lst._ids.append(types.InputMessageReplyTo(id=self.id))
return (await lst)[0]
return None
@ -415,7 +416,7 @@ class Message(metaclass=NoPublicConstructor):
buttons=buttons,
)
async def forward(self, target: ChatLike) -> Message:
async def forward(self, target: Peer | PeerRef) -> Message:
"""
Alias for :meth:`telethon.Client.forward_messages`.

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Optional, Self, Sequence
from ...session import PeerRef
from ...session import ChannelRef, GroupRef
from ...tl import abcs, types
from .admin_right import AdminRight
from .chat_restriction import ChatRestriction
@ -24,7 +24,7 @@ class Participant(metaclass=NoPublicConstructor):
def __init__(
self,
client: Client,
chat: PeerRef,
chat: GroupRef | ChannelRef,
participant: (
types.ChannelParticipant
| types.ChannelParticipantSelf
@ -47,7 +47,7 @@ class Participant(metaclass=NoPublicConstructor):
def _from_raw_channel(
cls,
client: Client,
chat: PeerRef,
chat: ChannelRef,
participant: abcs.ChannelParticipant,
chat_map: dict[int, Peer],
) -> Self:
@ -70,7 +70,7 @@ class Participant(metaclass=NoPublicConstructor):
def _from_raw_chat(
cls,
client: Client,
chat: PeerRef,
chat: GroupRef,
participant: abcs.ChatParticipant,
chat_map: dict[int, Peer],
) -> Self:
@ -193,7 +193,14 @@ class Participant(metaclass=NoPublicConstructor):
"""
participant = self.user or self.banned or self.left
assert participant
await self._client.set_participant_admin_rights(self._chat, participant, rights)
if isinstance(participant, User):
await self._client.set_participant_admin_rights(
self._chat, participant, rights
)
else:
raise TypeError(
f"participant of type {participant.__class__.__name__} cannot be made admin"
)
async def set_restrictions(
self,

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import abc
from typing import Optional
from ....session import PeerRef
from ....session import ChannelRef, GroupRef, UserRef
class Peer(abc.ABC):
@ -15,7 +15,7 @@ class Peer(abc.ABC):
@abc.abstractmethod
def id(self) -> int:
"""
The chat's integer identifier.
The peer's integer identifier.
This identifier is always a positive number.
@ -39,20 +39,28 @@ class Peer(abc.ABC):
@abc.abstractmethod
def username(self) -> Optional[str]:
"""
The primary *@username* of the chat.
The primary *@username* of the user, group or chat.
The returned string will *not* contain the at-sign ``@``.
"""
@property
@abc.abstractmethod
def pack(self) -> Optional[PeerRef]:
def ref(self) -> UserRef | GroupRef | ChannelRef:
"""
Pack the chat into a compact and reusable object.
The reusable reference to this user, group or channel.
This object can be easily serialized and saved to persistent storage.
Unlike resolving usernames, packed chats can be reused without costly calls.
This can be used to persist the reference to a database or disk,
or to create inline mentions.
.. seealso::
:doc:`/concepts/chats`
:doc:`/concepts/peers`
"""
@property
def _ref(self) -> UserRef | GroupRef | ChannelRef:
"""
Private alias that also exists in refs to make conversion trivial.
"""
return self.ref

View File

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

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 (
BOT_CHANNEL_DIFF_LIMIT,
NO_UPDATES_TIMEOUT,
@ -14,9 +22,13 @@ from .session import ChannelState, DataCenter, Session, UpdateState, User
from .storage import MemorySession, SqliteSession, Storage
__all__ = [
"ChannelRef",
"ChatHashCache",
"GroupRef",
"PeerAuth",
"PeerIdentifier",
"PeerRef",
"PackedType",
"UserRef",
"BOT_CHANNEL_DIFF_LIMIT",
"NO_UPDATES_TIMEOUT",
"USER_CHANNEL_DIFF_LIMIT",

View File

@ -1,4 +1,12 @@
from .hash_cache import ChatHashCache
from .peer_ref import PackedType, PeerRef
from .peer_ref import ChannelRef, GroupRef, PeerAuth, PeerIdentifier, PeerRef, UserRef
__all__ = ["ChatHashCache", "PeerRef", "PackedType"]
__all__ = [
"ChatHashCache",
"ChannelRef",
"GroupRef",
"PeerAuth",
"PeerIdentifier",
"PeerRef",
"UserRef",
]

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

View File

@ -1,150 +1,254 @@
from __future__ import annotations
import abc
import base64
import re
import struct
from enum import IntFlag
from typing import Optional, Self
from typing import Optional, Self, TypeAlias
from ...tl import abcs, types
PeerIdentifier: TypeAlias = int
PeerAuth: TypeAlias = Optional[int]
class PackedType(IntFlag):
"""
The type of a :class:`PeerRef`.
USER_PREFIX = "u."
GROUP_PREFIX = "g."
CHANNEL_PREFIX = "c."
class PeerRef(abc.ABC):
"""
A reference to a :term:`peer`.
# bits: zero, has-access-hash, channel, broadcast, group, chat, user, bot
USER = 0b0000_0010
BOT = 0b0000_0011
CHAT = 0b0000_0100
MEGAGROUP = 0b0010_1000
BROADCAST = 0b0011_0000
GIGAGROUP = 0b0011_1000
References can be used to interact with any method that expects a peer,
without the need to fetch or resolve the entire peer beforehand.
A reference consists of both an identifier and the authorization to access the peer.
The proof of authorization is represented by Telegram's access hash witness.
class PeerRef:
"""
A compact representation of a :term:`chat`.
You can reuse it as many times as you want.
You can call ``chat.pack()`` on :class:`~telethon.types.User`,
You can access the :attr:`telethon.types.Peer.ref` attribute on :class:`~telethon.types.User`,
:class:`~telethon.types.Group` or :class:`~telethon.types.Channel` to obtain it.
Not all references are always valid in all contexts.
Under certain conditions, it is possible for a reference without an authorization to be usable,
and for a reference with an authorization to not be usable everywhere.
The exact rules are defined by Telegram and could change any time.
.. seealso::
:doc:`/concepts/chats`
:doc:`/concepts/peers`
"""
__slots__ = ("ty", "id", "access_hash")
__slots__ = ("identifier", "authorization")
def __init__(self, ty: PackedType, id: int, access_hash: Optional[int]) -> None:
self.ty = ty
self.id = id
self.access_hash = access_hash
def __bytes__(self) -> bytes:
return struct.pack(
"<Bqq",
self.ty.value | (0 if self.access_hash is None else 0b0100_0000),
self.id,
self.access_hash or 0,
)
def __init__(
self, identifier: PeerIdentifier, authorization: PeerAuth = None
) -> None:
assert (
identifier >= 0
), "PeerRef identifiers must be positive; see the documentation for Peers"
self.identifier = identifier
self.authorization = authorization
@classmethod
def from_bytes(cls, data: bytes) -> Self:
ty_byte, id, access_hash = struct.unpack("<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:
def from_str(cls, string: str, /) -> UserRef | GroupRef | ChannelRef:
"""
Convenience property to convert to bytes and represent them as hexadecimal numbers:
Create a reference back from its string representation:
.. code-block::
:param string:
The :class:`str` representation of the :class:`PeerRef`.
assert packed.hex == bytes(packed).hex()
.. rubric:: Example
.. code-block:: python
ref: PeerRef = ...
assert PeerRef.from_str(str(ref)) == ref
"""
return bytes(self).hex()
if match := re.match(r"(\w\.)(\d+)\.([^.]+)", string):
prefix, iden, auth = match.groups()
identifier = int(iden)
if auth == "0":
authorization: Optional[int] = None
else:
try:
(authorization,) = struct.unpack(
"!q", base64.urlsafe_b64decode(auth.encode("ascii") + b"=")
)
except Exception:
raise ValueError(f"invalid PeerRef string: {string!r}")
if prefix == USER_PREFIX:
return UserRef(identifier, authorization)
elif prefix == GROUP_PREFIX:
return GroupRef(identifier, authorization)
elif prefix == CHANNEL_PREFIX:
return ChannelRef(identifier, authorization)
raise ValueError(f"invalid PeerRef string: {string!r}")
@classmethod
def from_hex(cls, hex: str) -> Self:
"""
Convenience method to convert hexadecimal numbers into bytes then passed to :meth:`from_bytes`:
:param hex:
Hexadecimal numbers to convert from.
.. code-block::
assert PackedChat.from_hex(packed.hex) == packed
"""
return cls.from_bytes(bytes.fromhex(hex))
def is_user(self) -> bool:
return self.ty in (PackedType.USER, PackedType.BOT)
def is_chat(self) -> bool:
return self.ty in (PackedType.CHAT,)
def is_channel(self) -> bool:
return self.ty in (
PackedType.MEGAGROUP,
PackedType.BROADCAST,
PackedType.GIGAGROUP,
)
def _empty_from_peer(cls, peer: abcs.Peer) -> UserRef | GroupRef | ChannelRef:
if isinstance(peer, types.PeerUser):
return UserRef(peer.user_id, None)
elif isinstance(peer, types.PeerChat):
return GroupRef(peer.chat_id, None)
elif isinstance(peer, types.PeerChannel):
return ChannelRef(peer.channel_id, None)
else:
raise RuntimeError("unexpected case")
@abc.abstractmethod
def _to_peer(self) -> abcs.Peer:
if self.is_user():
return types.PeerUser(user_id=self.id)
elif self.is_chat():
return types.PeerChat(chat_id=self.id)
elif self.is_channel():
return types.PeerChannel(channel_id=self.id)
else:
raise RuntimeError("unexpected case")
pass
@abc.abstractmethod
def _to_input_peer(self) -> abcs.InputPeer:
if self.is_user():
return types.InputPeerUser(
user_id=self.id, access_hash=self.access_hash or 0
)
elif self.is_chat():
return types.InputPeerChat(chat_id=self.id)
elif self.is_channel():
return types.InputPeerChannel(
channel_id=self.id, access_hash=self.access_hash or 0
)
else:
raise RuntimeError("unexpected case")
pass
def _to_input_user(self) -> types.InputUser:
if self.is_user():
return types.InputUser(user_id=self.id, access_hash=self.access_hash or 0)
else:
raise TypeError("chat is not a user")
@abc.abstractmethod
def __str__(self) -> str:
"""
Format the reference into a :class:`str`.
def _to_chat_id(self) -> int:
if self.is_chat():
return self.id
else:
raise TypeError("chat is not a group")
.. seealso::
def _to_input_channel(self) -> types.InputChannel:
if self.is_channel():
return types.InputChannel(
channel_id=self.id, access_hash=self.access_hash or 0
:doc:`/concepts/messages`, to learn how this can be used to format inline mentions in messages.
"""
def __repr__(self) -> str:
"""
Format the reference in a way that's easy to debug.
"""
return f"{self.__class__.__name__}({self.identifier}, {self.authorization})"
def _encode_str(self) -> str:
if self.authorization is None:
auth = "0"
else:
auth = (
base64.urlsafe_b64encode(struct.pack("!q", self.authorization))
.decode("ascii")
.rstrip("=")
)
else:
raise TypeError("chat is not a channel")
return f"{self.identifier}.{auth}"
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
return NotImplemented
return (
self.ty == other.ty
and self.id == other.id
and self.access_hash == other.access_hash
self.identifier == other.identifier
and self.authorization == other.authorization
)
@property
def _ref(self) -> UserRef | GroupRef | ChannelRef:
assert isinstance(self, (UserRef, GroupRef, ChannelRef))
return self
class UserRef(PeerRef):
"""
A user reference.
This includes both user accounts and bot accounts, and corresponds to a bare Telegram :tl:`user`.
"""
@classmethod
def from_str(cls, string: str, /) -> Self:
ref = super().from_str(string)
if not isinstance(ref, cls):
raise TypeError("PeerRef string does not belong to UserRef")
return ref
def _to_peer(self) -> abcs.Peer:
return types.PeerUser(user_id=self.identifier)
def _to_input_peer(self) -> abcs.InputPeer:
return types.InputPeerUser(
user_id=self.identifier, access_hash=self.authorization or 0
)
def _to_input_user(self) -> types.InputUser:
return types.InputUser(
user_id=self.identifier, access_hash=self.authorization or 0
)
def __str__(self) -> str:
return f"PackedChat.{self.ty.name}({self.id})"
return f"{USER_PREFIX}{self._encode_str()}"
@property
def _ref(self) -> Self:
return self
class GroupRef(PeerRef):
"""
A group reference.
This only includes small group chats, and corresponds to a bare Telegram :tl:`chat`.
"""
@classmethod
def from_str(cls, string: str, /) -> Self:
ref = super().from_str(string)
if not isinstance(ref, cls):
raise TypeError("PeerRef string does not belong to GroupRef")
return ref
def _to_peer(self) -> abcs.Peer:
return types.PeerChat(chat_id=self.identifier)
def _to_input_peer(self) -> abcs.InputPeer:
return types.InputPeerChat(chat_id=self.identifier)
def _to_input_chat(self) -> int:
return self.identifier
def __str__(self) -> str:
return f"{GROUP_PREFIX}{self._encode_str()}"
@property
def _ref(self) -> Self:
return self
class ChannelRef(PeerRef):
"""
A channel reference.
This includes broadcast channels, megagroups and gigagroups, and corresponds to a bare Telegram :tl:`channel`.
"""
@classmethod
def from_str(cls, string: str, /) -> Self:
ref = super().from_str(string)
if not isinstance(ref, cls):
raise TypeError("PeerRef string does not belong to ChannelRef")
return ref
def _to_peer(self) -> abcs.Peer:
return types.PeerChannel(channel_id=self.identifier)
def _to_input_peer(self) -> abcs.InputPeer:
return types.InputPeerChannel(
channel_id=self.identifier, access_hash=self.authorization or 0
)
def _to_input_channel(self) -> types.InputChannel:
return types.InputChannel(
channel_id=self.identifier, access_hash=self.authorization or 0
)
def __str__(self) -> str:
return f"{CHANNEL_PREFIX}{self._encode_str()}"
@property
def _ref(self) -> Self:
return self

View File

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

View File

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

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