Telethon/telethon/client/users.py
Dmitry D. Chernov c8f16a4e89 Fix a couple of inconsistencies in the public interface (#1102)
* Create `_NOT_A_REQUEST` when needed. Currently, modifications    
  in the raised exception would be "global".    
* `retries` parameters were actually attempts. This has been fixed    
  to actually be the amount of retries, so 0 now means don't retry.    
* Helper function to deal with retries instead of using a range with    
  different styles every time.
2019-02-06 19:41:45 +01:00

480 lines
20 KiB
Python

import asyncio
import itertools
import time
from .telegrambaseclient import TelegramBaseClient
from .. import errors, utils
from ..errors import MultiError, RPCError
from ..tl import TLObject, TLRequest, types, functions
from ..helpers import retry_range
_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!')
class UserMethods(TelegramBaseClient):
async def __call__(self, request, ordered=False):
requests = (request if utils.is_list_like(request) else (request,))
for r in requests:
if not isinstance(r, TLRequest):
raise _NOT_A_REQUEST()
await r.resolve(self, utils)
# Avoid making the request if it's already in a flood wait
if r.CONSTRUCTOR_ID in self._flood_waited_requests:
due = self._flood_waited_requests[r.CONSTRUCTOR_ID]
diff = round(due - time.time())
if diff <= 3: # Flood waits below 3 seconds are "ignored"
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
elif diff <= self.flood_sleep_threshold:
self._log[__name__].info(
'Sleeping early for %ds on flood wait', diff)
await asyncio.sleep(diff, loop=self._loop)
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
else:
raise errors.FloodWaitError(r, capture=diff)
request_index = 0
self._last_request = time.time()
for attempt in retry_range(self._request_retries):
try:
future = self._sender.send(request, ordered=ordered)
if isinstance(future, list):
results = []
exceptions = []
for f in future:
try:
result = await f
except RPCError as e:
exceptions.append(e)
results.append(None)
continue
self.session.process_entities(result)
exceptions.append(None)
results.append(result)
request_index += 1
if any(x is not None for x in exceptions):
raise MultiError(exceptions, results, requests)
else:
return results
else:
result = await future
self.session.process_entities(result)
return result
except (errors.ServerError, errors.RpcCallFailError) as e:
self._log[__name__].warning(
'Telegram is having internal issues %s: %s',
e.__class__.__name__, e)
except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e:
if utils.is_list_like(request):
request = request[request_index]
self._flood_waited_requests\
[request.CONSTRUCTOR_ID] = time.time() + e.seconds
if e.seconds <= self.flood_sleep_threshold:
self._log[__name__].info('Sleeping for %ds on flood wait',
e.seconds)
await asyncio.sleep(e.seconds, loop=self._loop)
else:
raise
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
errors.UserMigrateError) as e:
self._log[__name__].info('Phone migrated to %d', e.new_dc)
should_raise = isinstance(e, (
errors.PhoneMigrateError, errors.NetworkMigrateError
))
if should_raise and await self.is_user_authorized():
raise
await self._switch_dc(e.new_dc)
raise ValueError('Request was unsuccessful {} time(s)'
.format(attempt))
# region Public methods
async def get_me(self, input_peer=False):
"""
Gets "me" (the self user) which is currently authenticated,
or None if the request fails (hence, not authenticated).
Args:
input_peer (`bool`, optional):
Whether to return the :tl:`InputPeerUser` version or the normal
:tl:`User`. This can be useful if you just need to know the ID
of yourself.
Returns:
Your own :tl:`User`.
"""
if input_peer and self._self_input_peer:
return self._self_input_peer
try:
me = (await self(
functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
self._bot = me.bot
if not self._self_input_peer:
self._self_input_peer = utils.get_input_peer(
me, allow_self=False
)
return self._self_input_peer if input_peer else me
except errors.UnauthorizedError:
return None
async def is_bot(self):
"""
Return ``True`` if the signed-in user is a bot, ``False`` otherwise.
"""
if self._bot is None:
self._bot = (await self.get_me()).bot
return self._bot
async def is_user_authorized(self):
"""
Returns ``True`` if the user is authorized.
"""
if self._authorized is None:
try:
self._state = await self(functions.updates.GetStateRequest())
self._authorized = True
except errors.RPCError:
self._authorized = False
return self._authorized
async def get_entity(self, 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.
entity (`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) <get_input_entity>` which will
use the cache), and then use `get_entity` with the result of the
previous call.
Similar limits apply to invite links, and you should use their
ID instead.
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.
Unsupported types will raise ``TypeError``.
If the entity 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.
"""
single = not utils.is_list_like(entity)
if single:
entity = (entity,)
# Group input entities by string (resolve username),
# input users (get users), input chat (get chats) and
# input channels (get channels) to get the most entities
# in the less amount of calls possible.
inputs = []
for x in entity:
if isinstance(x, str):
inputs.append(x)
else:
inputs.append(await self.get_input_entity(x))
users = [x for x in inputs
if isinstance(x, (types.InputPeerUser, types.InputPeerSelf))]
chats = [x.chat_id for x in inputs
if isinstance(x, types.InputPeerChat)]
channels = [x for x in inputs
if isinstance(x, types.InputPeerChannel)]
if users:
# GetUsersRequest has a limit of 200 per call
tmp = []
while users:
curr, users = users[:200], users[200:]
tmp.extend(await self(functions.users.GetUsersRequest(curr)))
users = tmp
if chats: # TODO Handle chats slice?
chats = (await self(
functions.messages.GetChatsRequest(chats))).chats
if channels:
channels = (await self(
functions.channels.GetChannelsRequest(channels))).chats
# Merge users, chats and channels into a single dictionary
id_entity = {
utils.get_peer_id(x): x
for x in itertools.chain(users, chats, channels)
}
# We could check saved usernames and put them into the users,
# chats and channels list from before. While this would reduce
# the amount of ResolveUsername calls, it would fail to catch
# username changes.
result = []
for x in inputs:
if isinstance(x, str):
result.append(await self._get_entity_from_string(x))
elif not isinstance(x, types.InputPeerSelf):
result.append(id_entity[utils.get_peer_id(x)])
else:
result.append(next(
u for u in id_entity.values()
if isinstance(u, types.User) and u.is_self
))
return result[0] if single else result
async def get_input_entity(self, peer):
"""
Turns the given peer into its input entity version. Most requests
use this kind of :tl:`InputPeer`, so this is the most suitable call
to make for those cases. **Generally you should let the library do
its job** and don't worry about getting the input entity first, but
if you're going to use an entity often, consider making the call:
>>> import asyncio
>>> rc = asyncio.get_event_loop().run_until_complete
>>>
>>> from telethon import TelegramClient
>>> client = TelegramClient(...)
>>> # If you're going to use "username" often in your code
>>> # (make a lot of calls), consider getting its input entity
>>> # once, and then using the "user" everywhere instead.
>>> user = rc(client.get_input_entity('username'))
>>> # The same applies to IDs, chats or channels.
>>> chat = rc(client.get_input_entity(-123456789))
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
If a username or invite link is given, **the library will
use the cache**. This means that it's possible to be using
a username that *changed* or an old invite link (this only
happens if an invite link for a small group chat is used
after it was upgraded to a mega-group).
If the username or ID from the invite link is not found in
the cache, it will be fetched. The same rules apply to phone
numbers (``'+34 123456789'``) from people in your contact list.
If an exact name is given, it must be in the cache too. This
is not reliable as different people can share the same name
and which entity is returned is arbitrary, and should be used
only for quick tests.
If a positive integer ID is given, the entity will be searched
in cached users, chats or channels, without making any call.
If a negative integer ID is given, the entity will be searched
exactly as either a chat (prefixed with ``-``) or as a channel
(prefixed with ``-100``).
If a :tl:`Peer` is given, it will be searched exactly in the
cache as either a user, chat or channel.
If the given object can be turned into an input entity directly,
said operation will be done.
Unsupported types will raise ``TypeError``.
If the entity can't be found, ``ValueError`` will be raised.
Returns:
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`
or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``.
If you need to get the ID of yourself, you should use
`get_me` with ``input_peer=True``) instead.
"""
if peer in ('me', 'self'):
return types.InputPeerSelf()
try:
# First try to get the entity from cache, otherwise figure it out
return self.session.get_input_entity(peer)
except ValueError:
pass
if isinstance(peer, str):
return utils.get_input_peer(
await self._get_entity_from_string(peer))
if not isinstance(peer, int) and (not isinstance(peer, TLObject)
or peer.SUBCLASS_OF_ID != 0x2d45687):
# Try casting the object into an input peer. Might TypeError.
# Don't do it if a not-found ID was given (instead ValueError).
# Also ignore Peer (0x2d45687 == crc32(b'Peer'))'s, lacking hash.
return utils.get_input_peer(peer)
# If we're a bot and the user has messaged us privately users.getUsers
# will work with access_hash = 0. Similar for channels.getChannels.
# If we're not a bot but the user is in our contacts, it seems to work
# regardless. These are the only two special-cased requests.
peer = utils.get_peer(peer)
if isinstance(peer, types.PeerUser):
users = await self(functions.users.GetUsersRequest([
types.InputUser(peer.user_id, access_hash=0)]))
if users and not isinstance(users[0], types.UserEmpty):
# If the user passed a valid ID they expect to work for
# channels but would be valid for users, we get UserEmpty.
# Avoid returning the invalid empty input peer for that.
#
# We *could* try to guess if it's a channel first, and if
# it's not, work as a chat and try to validate it through
# another request, but that becomes too much work.
return utils.get_input_peer(users[0])
elif isinstance(peer, types.PeerChat):
return types.InputPeerChat(peer.chat_id)
elif isinstance(peer, types.PeerChannel):
try:
channels = await self(functions.channels.GetChannelsRequest([
types.InputChannel(peer.channel_id, access_hash=0)]))
return utils.get_input_peer(channels.chats[0])
except errors.ChannelInvalidError:
pass
raise ValueError(
'Could not find the input entity for {!r}. Please read https://'
'telethon.readthedocs.io/en/latest/extra/basic/entities.html to'
' find out more details.'
.format(peer)
)
async def get_peer_id(self, peer, add_mark=True):
"""
Gets the ID for the given peer, which may be anything entity-like.
This method needs to be ``async`` because `peer` supports usernames,
invite-links, phone numbers (from people in your contact list), etc.
If ``add_mark is False``, then a positive ID will be returned
instead. By default, bot-API style IDs (signed) are returned.
"""
if isinstance(peer, int):
return utils.get_peer_id(peer, add_mark=add_mark)
try:
if peer.SUBCLASS_OF_ID in (0x2d45687, 0xc91c90b6):
# 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer'
return utils.get_peer_id(peer)
except AttributeError:
pass
peer = await self.get_input_entity(peer)
if isinstance(peer, types.InputPeerSelf):
peer = await self.get_me(input_peer=True)
return utils.get_peer_id(peer, add_mark=add_mark)
# endregion
# region Private methods
async def _get_entity_from_string(self, string):
"""
Gets a full entity from the given string, which may be a phone or
a username, and processes all the found entities on the session.
The string may also be a user link, or a channel/chat invite link.
This method has the side effect of adding the found users to the
session database, so it can be queried later without API calls,
if this option is enabled on the session.
Returns the found entity, or raises TypeError if not found.
"""
phone = utils.parse_phone(string)
if phone:
try:
for user in (await self(
functions.contacts.GetContactsRequest(0))).users:
if user.phone == phone:
return user
except errors.BotMethodInvalidError:
raise ValueError('Cannot get entity by phone number as a '
'bot (try using integer IDs, not strings)')
elif string.lower() in ('me', 'self'):
return await self.get_me()
else:
username, is_join_chat = utils.parse_username(string)
if is_join_chat:
invite = await self(
functions.messages.CheckChatInviteRequest(username))
if isinstance(invite, types.ChatInvite):
raise ValueError(
'Cannot get entity from a channel (or group) '
'that you are not part of. Join the group and retry'
)
elif isinstance(invite, types.ChatInviteAlready):
return invite.chat
elif username:
try:
result = await self(
functions.contacts.ResolveUsernameRequest(username))
except errors.UsernameNotOccupiedError as e:
raise ValueError('No user has "{}" as username'
.format(username)) from e
try:
pid = utils.get_peer_id(result.peer, add_mark=False)
if isinstance(result.peer, types.PeerUser):
return next(x for x in result.users if x.id == pid)
else:
return next(x for x in result.chats if x.id == pid)
except StopIteration:
pass
try:
# Nobody with this username, maybe it's an exact name/title
return await self.get_entity(
self.session.get_input_entity(string))
except ValueError:
pass
raise ValueError(
'Cannot find any entity corresponding to "{}"'.format(string)
)
async def _get_input_dialog(self, dialog):
"""
Returns a :tl:`InputDialogPeer`. This is a bit tricky because
it may or not need access to the client to convert what's given
into an input entity.
"""
try:
if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer')
dialog.peer = await self.get_input_entity(dialog.peer)
return dialog
elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
return types.InputDialogPeer(dialog)
except AttributeError:
pass
return types.InputDialogPeer(await self.get_input_entity(dialog))
async def _get_input_notify(self, notify):
"""
Returns a :tl:`InputNotifyPeer`. This is a bit tricky because
it may or not need access to the client to convert what's given
into an input entity.
"""
try:
if notify.SUBCLASS_OF_ID == 0x58981615:
if isinstance(notify, types.InputNotifyPeer):
notify.peer = await self.get_input_entity(notify.peer)
return notify
except AttributeError:
return types.InputNotifyPeer(await self.get_input_entity(notify))
# endregion