diff --git a/readthedocs/concepts/entities.rst b/readthedocs/concepts/entities.rst index f76c3983..bd60072a 100644 --- a/readthedocs/concepts/entities.rst +++ b/readthedocs/concepts/entities.rst @@ -1,20 +1,27 @@ .. _entities: -======== -Entities -======== +=============== +Users and Chats +=============== -The library widely uses the concept of "entities". An entity will refer -to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return -in response to certain methods, such as :tl:`GetUsersRequest`. +The library widely uses the concept of "users" to refer to both real accounts +and bot accounts, as well as the concept of "chats" to refer to groups and +broadcast channels. + +The most general term you can use to think about these is "an entity", but +recent versions of the library often prefer to opt for names which better +reflect the intention, such as "dialog" when a previously-existing +conversation is expected, or "profile" when referring to the information about +the user or chat. .. note:: - When something "entity-like" is required, it means that you need to - provide something that can be turned into an entity. These things include, - but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects, - or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even - phone numbers **from people you have in your contact list**. + When something "dialog-like" is required, it means that you need to + provide something that can be used to refer to an open conversation. + These things include, but are not limited to, packed chats, usernames, + integer IDs (identifiers), :tl:`Peer` objects, or even entire :tl:`User`, + :tl:`Chat` and :tl:`Channel` objects and even phone numbers **from people + you have in your contact list**. To "encounter" an ID, you would have to "find it" like you would in the normal app. If the peer is in your dialogs, you would need to @@ -23,82 +30,123 @@ in response to certain methods, such as :tl:`GetUsersRequest`. `client.get_participants(group) `. Once you have encountered an ID, the library will (by default) have saved - their ``access_hash`` for you, which is needed to invoke most methods. + its packed version for you, which is needed to invoke most methods. This is why sometimes you might encounter this error when working with the library. You should ``except ValueError`` and run code that you know - should work to find the entity. + should work to find the user or chat. You **cannot** use an ID of someone + you haven't interacted with. Because this is more unreliable, packed chats + are recommended instead. .. contents:: -What is an Entity? -================== +What is a User? +=============== -A lot of methods and requests require *entities* to work. For example, -you send a message to an *entity*, get the username of an *entity*, and -so on. +A `User ` can be either a real user account +(some person who has signed up for an account) or a bot account which is +programmed to perform certain actions (created by a developer via +`@BotFather `_). -There are many things that work as entities: usernames, phone numbers, -chat links, invite links, IDs, and the types themselves. That is, you can -use any of those when you see an "entity" is needed. +A lot of methods and requests require user or chats to work. For example, +you can send a message to a *user*, ban a *user* from a group, and so on. +These methods accept more than just `User ` +as the input parameter. You can also use packed users, usernames, string phone +numbers, or integer IDs, although some have higher cost than others. + +When using the username, the library must fetch it first, which can be +expensive. When using the phone number, the library must fetch it first, which +can be expensive. If you plan to use these, it's recommended you manually use +`client.get_profile() ` to cache +the username or phone number, and then use the value returned instead. .. note:: Remember that the phone number must be in your contact list before you can use it. -You should use, **from better to worse**: +The recommended type to use as input parameters to the methods is either a +`User ` instance or its packed type. -1. Input entities. For example, `event.input_chat - `, - `message.input_sender - `, - or caching an entity you will use a lot with - ``entity = await client.get_input_entity(...)``. - -2. Entities. For example, if you had to get someone's - username, you can use ``user`` or ``channel``. - It will work. Only use this option if you already have the entity! - -3. IDs. This will always look the entity up from the - cache (the ``*.session`` file caches seen entities). - -4. Usernames, phone numbers, and links. The cache will be - used too (unless you force a `client.get_entity() - `), - but may make a request if the username, phone, or link - has not been found yet. - -In recent versions of the library, the following two are equivalent: - -.. code-block:: python - - async def handler(event): - await client.send_message(event.sender_id, 'Hi') - await client.send_message(event.input_sender, 'Hi') +In the raw API, users are instances of :tl:`User` (or :tl:`UserEmpty`), which +are returned in response to some requests, such as :tl:`GetUsersRequest`. +There are also variants for use as "input parameters", such as :tl:`InputUser` +and :tl:`InputPeerUser`. You generally **do not need** to worry about these +types unless you're using raw API. -If you need to be 99% sure that the code will work (sometimes it's -simply impossible for the library to find the input entity), or if -you will reuse the chat a lot, consider using the following instead: +What is a Chat? +=============== -.. code-block:: python +A `Chat ` can be a small group chat (the +default group type created by users where many users can join and talk), a +megagroup (also known as "supergroup"), a broadcast channel or a broadcast +group. - async def handler(event): - # This method may make a network request to find the input sender. - # Properties can't make network requests, so we need a method. - sender = await event.get_input_sender() - await client.send_message(sender, 'Hi') - await client.send_message(sender, 'Hi') +The term "chat" is really overloaded in Telegram. The library tries to be +explicit and always use "small group chat", "megagroup" and "broadcast" to +differentiate. However, Telegram's API uses "chat" to refer to both "chat" +(small group chat), and "channel" (megagroup, broadcast or "gigagroup" which +is a broadcast group of type channel). + +A lot of methods and requests require a chat to work. For example, +you can get the participants from a *chat*, kick users from a *chat*, and so on. +These methods accept more than just `Chat ` +as the input parameter. You can also use packed chats, the public link, or +integer IDs, although some have higher cost than others. + +When using the public link, the library must fetch it first, which can be +expensive. If you plan to use these, it's recommended you manually use +`client.get_profile() ` to cache +the link, and then use the value returned instead. + +.. note:: + + The link of a public chat has the form "t.me/username", where the username + can belong to either an actual user or a public chat. + +The recommended type to use as input parameters to the methods is either a +`Chat ` instance or its packed type. + +In the raw API, chats are instances of :tl:`Chat` and :tl:`Channel` (or +:tl:`ChatEmpty`, :tl:`ChatForbidden` and :tl:`ChannelForbidden`), which +are returned in response to some requests, such as :tl:`messages.GetChats` +and :tl:`channels.GetChannels`. There are also variants for use as "input +parameters", such as :tl:`InputChannel` and :tl:`InputPeerChannel`. You +generally **do not need** to worry about these types unless you're using raw API. -Getting Entities -================ +When to use each term? +====================== + +The term "dialog" is used when the library expects a reference to an open +conversation (from the list the user sees when they open the application). + +The term "profile" is used instead of "dialog" when the conversation is not +expected to exist. Because "dialog" is more specific than "profile", "dialog" +is used where possible instead. + +In general, you should not use named arguments for neither "dialogs" or +"profiles", since they're the first argument. The parameter name only exists +for documentation purposes. + +The term "chat" is used where a group or broadcast channel is expected. This +includes small groups, megagroups, broadcast channels and broadcast groups. +Telegram's API has, in the past, made a difference between which methods can +be used for "small group chats" and everything else. For example, small group +chats cannot have a public link (they automatically convert to megagroups). +Group permissions also used to be different, but because Telegram may unify +these eventually, the library attempts to hide this distinction. In general, +this is not something you should worry about. + + +Fetching profile information +============================ Through the use of the :ref:`sessions`, the library will automatically -remember the ID and hash pair, along with some extra information, so -you're able to just do this: +remember the packed users and chats, along with some extra information, +so you're able to just do this: .. code-block:: python @@ -106,145 +154,37 @@ you're able to just do this: # # Dialogs are the "conversations you have open". # This method returns a list of Dialog, which - # has the .entity attribute and other information. + # has the .user and .chat attributes (among others). # - # This part is IMPORTANT, because it fills the entity cache. + # This part is IMPORTANT, because it fills the cache. dialogs = await client.get_dialogs() - # All of these work and do the same. - username = await client.get_entity('username') - username = await client.get_entity('t.me/username') - username = await client.get_entity('https://telegram.dog/username') + # All of these work and do the same, but are more expensive to use. + channel = await client.get_profile('username') + channel = await client.get_profile('t.me/username') + channel = await client.get_profile('https://telegram.dog/username') + contact = await client.get_profile('+34xxxxxxxxx') - # Other kind of entities. - channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') - contact = await client.get_entity('+34xxxxxxxxx') - friend = await client.get_entity(friend_id) + # This will work, but only if the ID is in cache. + friend = await client.get_profile(friend_id) - # Getting entities through their ID (User, Chat or Channel) - entity = await client.get_entity(some_id) - - # You can be more explicit about the type for said ID by wrapping - # it inside a Peer instance. This is recommended but not necessary. - from telethon.tl.types import PeerUser, PeerChat, PeerChannel - - my_user = await client.get_entity(PeerUser(some_id)) - my_chat = await client.get_entity(PeerChat(some_id)) - my_channel = await client.get_entity(PeerChannel(some_id)) + # This is the most reliable way to fetch a profile. + user = await client.get_profile('U.123.456789') + group = await client.get_profile('G.456.0') + broadcast = await client.get_profile('C.789.123456') -.. note:: - - You **don't** need to get the entity before using it! Just let the - library do its job. Use a phone from your contacts, username, ID or - input entity (preferred but not necessary), whatever you already have. - -All methods in the :ref:`telethon-client` call `.get_input_entity() -` prior -to sending the request to save you from the hassle of doing so manually. +All methods in the :ref:`telethon-client` accept any of the above +prior to sending the request to save you from the hassle of doing so manually. That way, convenience calls such as `client.send_message('username', 'hi!') -` -become possible. - -Every entity the library encounters (in any response to any call) will by -default be cached in the ``.session`` file (an SQLite database), to avoid -performing unnecessary API calls. If the entity cannot be found, additional -calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be -made to obtain the required information. - - -Entities vs. Input Entities -=========================== - -.. note:: - - This section is informative but worth reading. The library - will transparently handle all of these details for you. - -On top of the normal types, the API also make use of what they call their -``Input*`` versions of objects. The input version of an entity (e.g. -:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum -information required from Telegram to identify -who you're referring to: a :tl:`Peer`'s **ID** and **hash**. They -are named like this because they are input parameters in the requests. - -Entities' IDs are the same for all user and bot accounts. However, the access -hash is **different for each account**, so trying to reuse the access hash -from one account in another will **not** work. - -Sometimes, Telegram only needs to indicate the entity type and their ID. For this purpose, :tl:`Peer` versions of the entities also -exist, which just have the ID. You cannot get the hash out of them since -you should not need it. The library probably has cached it before. - -Peers are enough to identify an entity, but they are not enough to make -a request with them. You need to know their hash before you can -"use them", and to know the hash you need to "encounter" them, let it -be in your dialogs, participants, message forwards, etc. - -.. note:: - - You *can* use peers with the library. Behind the scenes, they are - replaced with the input variant. Peers "aren't enough" on their own - , but the library will do some more work to use the right type. - -As we just mentioned, API calls don't need to know the whole information -about the entities, only their ID and hash. For this reason, another method, -`client.get_input_entity() ` -is available. This will always use the cache while possible, making zero API -calls most of the time. When a request is made, if you provided the full -entity, e.g. an :tl:`User`, the library will automatically convert it to the required -:tl:`InputPeer`. - -**You should always favour** -`client.get_input_entity() ` -**over** -`client.get_entity() ` -for this reason! Calling the latter will always make an API call to get -the most recent information about said entity, but invoking requests don't -need this information, just the :tl:`InputPeer`. Only use -`client.get_entity() ` -if you need to get actual information, like the username, name, title, etc. -of the entity. - -To further simplify the workflow, since the version ``0.16.2`` of the -library, the raw requests you make to the API are also able to call -`client.get_input_entity() ` -wherever needed, so you can even do things like: - -.. code-block:: python - - await client(SendMessageRequest('username', 'hello')) - -The library will call the request's ``.resolve()`` method, which will -resolve ``'username'`` with the appropriate :tl:`InputPeer`. Don't worry if -you don't get this yet, but remember that some of the details here are important. - - -Full Entities -============= - -In addition to :tl:`PeerUser`, :tl:`InputPeerUser`, :tl:`User` (and its -variants for chats and channels), there is also the concept of :tl:`UserFull`. - -This full variant has additional information such as whether the user is -blocked, its notification settings, the bio or about of the user, etc. - -There is also :tl:`messages.ChatFull` which is the equivalent of full entities -for chats and channels, with the about section of the channel. Note that -the ``users`` field only contains bots for the channel (so clients can -suggest commands to use). - -You can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat` -and :tl:`GetFullChannel` respectively. - - -Accessing Entities -================== +` become possible. +However, it can be expensive to fetch the username every time, so this is +better left for things which are not executed often. Although it's explicitly noted in the documentation that messages *subclass* `ChatGetter ` and `SenderGetter `, -some people still don't get inheritance. +this section will explain what this means. When the documentation says "Bases: `telethon.tl.custom.chatgetter.ChatGetter`" it means that the class you're looking at, *also* can act as the class it @@ -257,9 +197,9 @@ That means you can do this: .. code-block:: python - message.is_private message.chat_id - await message.get_chat() + message.chat + await event.get_chat() # ...etc `SenderGetter ` is similar: @@ -267,8 +207,8 @@ That means you can do this: .. code-block:: python message.user_id - await message.get_input_user() message.user + await event.get_input_user() # ...etc Quite a few things implement them, so it makes sense to reuse the code. @@ -277,11 +217,51 @@ For example, all events (except raw updates) implement `ChatGetter in some chat. +Packed User and packed Chat +=========================== + +A packed `User ` or a packed +`Chat ` can be thought of as +"a small string reference to the actual user or chat". + +It can easily be saved or embedded in the code for later use, +without having to worry if the user is in the session file cache. + +This "packed representation" is a compact way to store the type of the User +or Chat (is it a user account, a bot, a broadcast channel…), the identifier, +and the access hash. This "access hash" is something Telegram uses to ensure +that you can actually use this "User" or "Chat" in requests (so you can't just +create some random user identifier and expect it to work). + +In the raw API, this is pretty much "input peers", but the library uses the +term "packed user or chat" to refer to its custom type and string +representation. + +The User and Chat IDs are the same for all user and bot accounts. However, the +access hash is **different for each account**, so trying to reuse the access +hash from one account in another will **not** work. This also means the packed +representation will only work for the account that created it. + +The library needs to have this access hash in some way for it to work. +If it only has an ID and this ID is not in cache, it will not work. +If using the packed representation, the hash is embedded, and will always work. + +Every method, including raw API, will automatically convert your types to the +expected input type the API uses, meaning the following will work: + + +.. code-block:: python + + await client(_tl.fn.messages.SendMessage('username', 'hello')) + +(This is only a raw API example, there are better ways to send messages.) + + Summary ======= -TL;DR; If you're here because of *"Could not find the input entity for"*, -you must ask yourself, "how did I find this entity through official +TL;DR; If you're here because of *"Could not find the input peer for"*, +you must ask yourself, "how did I find this user or chat through official applications"? Now do the same with the library. Use what applies: .. code-block:: python @@ -289,7 +269,7 @@ applications"? Now do the same with the library. Use what applies: # (These examples assume you are inside an "async def") async with client: # Does it have a username? Use it! - entity = await client.get_entity(username) + user = await client.get_profile(username) # Do you have a conversation open with them? Get dialogs. await client.get_dialogs() @@ -297,16 +277,20 @@ applications"? Now do the same with the library. Use what applies: # Are they participants of some group? Get them. await client.get_participants('username') - # Is the entity the original sender of a forwarded message? Get it. + # Is the user the original sender of a forwarded message? Fetch the message. await client.get_messages('username', 100) # NOW you can use the ID anywhere! await client.send_message(123456, 'Hi!') - entity = await client.get_entity(123456) - print(entity) + user = await client.get_profile(123456) + print(user) -Once the library has "seen" the entity, you can use their **integer** ID. -You can't use entities from IDs the library hasn't seen. You must make the -library see them *at least once* and disconnect properly. You know where -the entities are, and you must tell the library. It won't guess for you. +Once the library has "seen" the user or chat, you can use their **integer** ID. +You can't use users or chats from IDs the library hasn't seen. You must make +the library see them *at least once* and disconnect properly. You know where +the user or chat are, and you must tell the library. It won't guess for you. + +This is why it's recommended to use the packed versions instead. They will +always work (unless Telegram, for some very unlikely reason, changes the way +using users and chats works, of course). diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 8bad852b..86ee2194 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -987,3 +987,4 @@ start also mandates phone= or password= as kwarg. qrlogin expires has been replaced with timeout and expired for parity with tos and auth. the goal is to hide the error-prone system clock and instead use asyncio's clock. recreate was removed (just call qr_login again; parity with get_tos). class renamed to QrLogin. now must be used in a contextmgr to prevent misuse. "entity" parameters have been renamed to "dialog" (user or chat expected) or "chat" (only chats expected), "profile" (if that makes sense). the goal is to move away from the entity terminology. this is intended to be a documentation change, but because the parameters were renamed, it's breaking. the expected usage of positional arguments is mostly unaffected. this includes the EntityLike hint. download_media param renamed message to media. iter_download file to media too +less types are supported to get entity (exact names, private links are undocumented but may work). get_entity is get_profile. get_input_entity is gone. get_peer_id is gone (if the isntance needs to be fetched anyway just use get_profile). diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 28e89531..a4958fe2 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3279,57 +3279,49 @@ class TelegramClient: await client.sign_in(phone, code) """ - @forward_call(users.get_entity) - async def get_entity( + @forward_call(users.get_profile) + async def get_profile( self: 'TelegramClient', - entity: 'hints.EntitiesLike') -> 'hints.Entity': + profile: 'hints.DialogsLike') -> 'hints.Entity': """ - Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` - or :tl:`Channel`. You can also pass a list or iterable of entities, - and they will be efficiently fetched from the network. + Turns the given profile reference into a `User ` + or `Chat ` instance. Arguments - entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): + profile (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): If a username is given, **the username will be resolved** making an API call every time. Resolving usernames is an expensive operation and will start hitting flood waits around 50 usernames in a short period of time. - If you want to get the entity for a *cached* username, you should - first `get_input_entity(username) ` which will - use the cache), and then use `get_entity` with the result of the - previous call. + Using phone numbers with strings will fetch your contact list first. - Similar limits apply to invite links, and you should use their - ID instead. + Using integer IDs will only work if the ID is in the session cache. - Using phone numbers (from people in your contact list), exact - names, integer IDs or :tl:`Peer` rely on a `get_input_entity` - first, which in turn needs the entity to be in cache, unless - a :tl:`InputPeer` was passed. + ``'me'`` is a special-case to the logged-in account (yourself). Unsupported types will raise ``TypeError``. - If the entity can't be found, ``ValueError`` will be raised. + If the user or chat can't be found, ``ValueError`` will be raised. Returns - :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the - input entity. A list will be returned if more than one was given. + `User ` or `Chat `, + depending on the profile requested. Example .. code-block:: python from telethon import utils - me = await client.get_entity('me') + me = await client.get_profile('me') print(utils.get_display_name(me)) - chat = await client.get_input_entity('username') + chat = await client.get_profile('username') async for message in client.get_messages(chat): ... # Note that you could have used the username directly, but it's - # good to use get_input_entity if you will reuse it a lot. + # good to use get_profile if you will reuse it a lot. async for message in client.get_messages('username'): ... diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 154a778c..63138e53 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -150,9 +150,9 @@ async def is_user_authorized(self: 'TelegramClient') -> bool: except RpcError: return False -async def get_entity( +async def get_profile( self: 'TelegramClient', - entity: 'hints.EntitiesLike') -> 'hints.Entity': + profile: 'hints.DialogsLike') -> 'hints.Entity': single = not utils.is_list_like(entity) if single: entity = (entity,) @@ -222,7 +222,7 @@ async def get_entity( async def get_input_entity( self: 'TelegramClient', - peer: 'hints.EntityLike') -> '_tl.TypeInputPeer': + peer: 'hints.DialogLike') -> '_tl.TypeInputPeer': # Short-circuit if the input parameter directly maps to an InputPeer try: return utils.get_input_peer(peer) @@ -281,7 +281,7 @@ async def get_input_entity( pass raise ValueError( - 'Could not find the input entity for {} ({}). Please read https://' + 'Could not find the input peer for {} ({}). Please read https://' 'docs.telethon.dev/en/latest/concepts/entities.html to' ' find out more details.' .format(peer, type(peer).__name__) @@ -289,7 +289,7 @@ async def get_input_entity( async def get_peer_id( self: 'TelegramClient', - peer: 'hints.EntityLike') -> int: + peer: 'hints.DialogLike') -> int: if isinstance(peer, int): return utils.get_peer_id(peer)