mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-22 01:16:35 +03:00
Migrate from chat to peer
This commit is contained in:
parent
1dba3ae6d0
commit
38241dffd2
|
@ -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.
|
||||
|
|
|
@ -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 👍.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
|
40
client/tests/peer_ref_test.py
Normal file
40
client/tests/peer_ref_test.py
Normal 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))
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user