mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-26 03:13:45 +03:00
Separate user methods from the base client
This commit is contained in:
parent
4b147f0153
commit
bb9b9796e0
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
from .telegram_bare_client import TelegramBareClient
|
from .client.telegramclient import TelegramClient
|
||||||
from .telegram_client import TelegramClient
|
|
||||||
from .network import connection
|
from .network import connection
|
||||||
from . import tl, version
|
from . import tl, version
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""
|
||||||
|
This package defines clients as subclasses of others, and then a single
|
||||||
|
`telethon.client.telegramclient.TelegramClient` which is subclass of them
|
||||||
|
all to provide the final unified interface while the methods can live in
|
||||||
|
different subclasses to be more maintainable.
|
||||||
|
|
||||||
|
The ABC is `telethon.client.telegrambaseclient.TelegramBaseClient` and the
|
||||||
|
first implementor is `telethon.client.users.UserMethods`, since calling
|
||||||
|
requests require them to be resolved first, and that requires accessing
|
||||||
|
entities (users).
|
||||||
|
"""
|
|
@ -1,3 +1,4 @@
|
||||||
|
import abc
|
||||||
import asyncio
|
import asyncio
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
@ -5,15 +6,15 @@ import platform
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
from . import version, errors, utils
|
from .. import version, errors, utils
|
||||||
from .crypto import rsa
|
from ..crypto import rsa
|
||||||
from .extensions import markdown
|
from ..extensions import markdown
|
||||||
from .network import MTProtoSender, ConnectionTcpFull
|
from ..network import MTProtoSender, ConnectionTcpFull
|
||||||
from .network.mtprotostate import MTProtoState
|
from ..network.mtprotostate import MTProtoState
|
||||||
from .sessions import Session, SQLiteSession
|
from ..sessions import Session, SQLiteSession
|
||||||
from .tl import TLObject, types, functions
|
from ..tl import TLObject, types, functions
|
||||||
from .tl.all_tlobjects import LAYER
|
from ..tl.all_tlobjects import LAYER
|
||||||
from .update_state import UpdateState
|
from ..update_state import UpdateState
|
||||||
|
|
||||||
DEFAULT_DC_ID = 4
|
DEFAULT_DC_ID = 4
|
||||||
DEFAULT_IPV4_IP = '149.154.167.51'
|
DEFAULT_IPV4_IP = '149.154.167.51'
|
||||||
|
@ -23,10 +24,11 @@ DEFAULT_PORT = 443
|
||||||
__log__ = logging.getLogger(__name__)
|
__log__ = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TelegramBareClient:
|
class TelegramBaseClient(abc.ABC):
|
||||||
"""
|
"""
|
||||||
A bare Telegram client that somewhat eases the usage of the
|
This is the abstract base class for the client. It defines some
|
||||||
``MTProtoSender``.
|
basic stuff like connecting, switching data center, etc, and
|
||||||
|
leaves the `__call__` unimplemented.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session (`str` | `telethon.sessions.abstract.Session`, `None`):
|
session (`str` | `telethon.sessions.abstract.Session`, `None`):
|
||||||
|
@ -260,8 +262,8 @@ class TelegramBareClient:
|
||||||
|
|
||||||
async def _get_dc(self, dc_id, cdn=False):
|
async def _get_dc(self, dc_id, cdn=False):
|
||||||
"""Gets the Data Center (DC) associated to 'dc_id'"""
|
"""Gets the Data Center (DC) associated to 'dc_id'"""
|
||||||
if not TelegramBareClient._config:
|
if not TelegramBaseClient._config:
|
||||||
TelegramBareClient._config =\
|
TelegramBaseClient._config =\
|
||||||
await self(functions.help.GetConfigRequest())
|
await self(functions.help.GetConfigRequest())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -272,7 +274,7 @@ class TelegramBareClient:
|
||||||
rsa.add_key(pk.public_key)
|
rsa.add_key(pk.public_key)
|
||||||
|
|
||||||
return next(
|
return next(
|
||||||
dc for dc in TelegramBareClient._config.dc_options
|
dc for dc in TelegramBaseClient._config.dc_options
|
||||||
if dc.id == dc_id
|
if dc.id == dc_id
|
||||||
and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn
|
and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn
|
||||||
)
|
)
|
||||||
|
@ -281,7 +283,7 @@ class TelegramBareClient:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# New configuration, perhaps a new CDN was added?
|
# New configuration, perhaps a new CDN was added?
|
||||||
TelegramBareClient._config =\
|
TelegramBaseClient._config =\
|
||||||
await self(functions.help.GetConfigRequest())
|
await self(functions.help.GetConfigRequest())
|
||||||
|
|
||||||
return self._get_dc(dc_id, cdn=cdn)
|
return self._get_dc(dc_id, cdn=cdn)
|
||||||
|
@ -369,7 +371,8 @@ class TelegramBareClient:
|
||||||
|
|
||||||
# region Invoking Telegram requests
|
# region Invoking Telegram requests
|
||||||
|
|
||||||
async def __call__(self, request, retries=5, ordered=False):
|
@abc.abstractmethod
|
||||||
|
def __call__(self, request, retries=5, ordered=False):
|
||||||
"""
|
"""
|
||||||
Invokes (sends) one or more MTProtoRequests and returns (receives)
|
Invokes (sends) one or more MTProtoRequests and returns (receives)
|
||||||
their result.
|
their result.
|
||||||
|
@ -387,36 +390,7 @@ class TelegramBareClient:
|
||||||
The result of the request (often a `TLObject`) or a list of
|
The result of the request (often a `TLObject`) or a list of
|
||||||
results if more than one request was given.
|
results if more than one request was given.
|
||||||
"""
|
"""
|
||||||
requests = (request,) if not utils.is_list_like(request) else request
|
raise NotImplementedError
|
||||||
if not all(isinstance(x, TLObject) and
|
|
||||||
x.content_related for x in requests):
|
|
||||||
raise TypeError('You can only invoke requests, not types!')
|
|
||||||
|
|
||||||
for r in requests:
|
|
||||||
await r.resolve(self, utils)
|
|
||||||
|
|
||||||
for _ in range(retries):
|
|
||||||
try:
|
|
||||||
future = self._sender.send(request, ordered=ordered)
|
|
||||||
if isinstance(future, list):
|
|
||||||
results = []
|
|
||||||
for f in future:
|
|
||||||
results.append(await f)
|
|
||||||
return results
|
|
||||||
else:
|
|
||||||
return await future
|
|
||||||
except (errors.ServerError, errors.RpcCallFailError):
|
|
||||||
pass
|
|
||||||
except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e:
|
|
||||||
if e.seconds <= self.session.flood_sleep_threshold:
|
|
||||||
await asyncio.sleep(e.seconds)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
|
|
||||||
errors.UserMigrateError) as e:
|
|
||||||
await self._switch_dc(e.new_dc)
|
|
||||||
|
|
||||||
raise ValueError('Number of retries reached 0')
|
|
||||||
|
|
||||||
# Let people use client.invoke(SomeRequest()) instead client(...)
|
# Let people use client.invoke(SomeRequest()) instead client(...)
|
||||||
async def invoke(self, *args, **kwargs):
|
async def invoke(self, *args, **kwargs):
|
||||||
|
@ -425,227 +399,3 @@ class TelegramBareClient:
|
||||||
return await self(*args, **kwargs)
|
return await self(*args, **kwargs)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Minimal helpers
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
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 get_entity(self, entity):
|
|
||||||
"""
|
|
||||||
Turns the given entity into a valid Telegram user or chat.
|
|
||||||
|
|
||||||
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
|
|
||||||
The entity (or iterable of entities) to be transformed.
|
|
||||||
If it's a string which can be converted to an integer or starts
|
|
||||||
with '+' it will be resolved as if it were a phone number.
|
|
||||||
|
|
||||||
If it doesn't start with '+' or starts with a '@' it will be
|
|
||||||
be resolved from the username. If no exact match is returned,
|
|
||||||
an error will be raised.
|
|
||||||
|
|
||||||
If the entity is an integer or a Peer, its information will be
|
|
||||||
returned through a call to self.get_input_peer(entity).
|
|
||||||
|
|
||||||
If the entity is neither, and it's not a TLObject, an
|
|
||||||
error 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 = [
|
|
||||||
x if isinstance(x, str) else await self.get_input_entity(x)
|
|
||||||
for x in entity
|
|
||||||
]
|
|
||||||
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 = [
|
|
||||||
await self._get_entity_from_string(x) if isinstance(x, str)
|
|
||||||
else (
|
|
||||||
id_entity[utils.get_peer_id(x)]
|
|
||||||
if not isinstance(x, types.InputPeerSelf)
|
|
||||||
else next(u for u in id_entity.values()
|
|
||||||
if isinstance(u, types.User) and u.is_self)
|
|
||||||
)
|
|
||||||
for x in inputs
|
|
||||||
]
|
|
||||||
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 InputUser, InputChat and so on, so this is the
|
|
||||||
most suitable call to make for those cases.
|
|
||||||
|
|
||||||
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
|
|
||||||
The integer ID of an user or otherwise either of a
|
|
||||||
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for
|
|
||||||
which to get its ``Input*`` version.
|
|
||||||
|
|
||||||
If this ``Peer`` hasn't been seen before by the library, the top
|
|
||||||
dialogs will be loaded and their entities saved to the session
|
|
||||||
file (unless this feature was disabled explicitly).
|
|
||||||
|
|
||||||
If in the end the access hash required for the peer was not found,
|
|
||||||
a 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)
|
|
||||||
|
|
||||||
raise ValueError(
|
|
||||||
'Could not find the input entity for "{}". Please read https://'
|
|
||||||
'telethon.readthedocs.io/en/latest/extra/basic/entities.html to'
|
|
||||||
' find out more details.'
|
|
||||||
.format(peer)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
an 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:
|
|
||||||
for user in (await self(
|
|
||||||
functions.contacts.GetContactsRequest(0))).users:
|
|
||||||
if user.phone == phone:
|
|
||||||
return user
|
|
||||||
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:
|
|
||||||
if username in ('me', 'self'):
|
|
||||||
return await self.get_me()
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = await self(
|
|
||||||
functions.contacts.ResolveUsernameRequest(username))
|
|
||||||
except errors.UsernameNotOccupiedError as e:
|
|
||||||
raise ValueError('No user has "{}" as username'
|
|
||||||
.format(username)) from e
|
|
||||||
|
|
||||||
for entity in itertools.chain(result.users, result.chats):
|
|
||||||
if getattr(entity, 'username', None) or '' \
|
|
||||||
.lower() == username:
|
|
||||||
return entity
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
|
@ -11,17 +11,17 @@ from collections import UserList
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
|
|
||||||
from .crypto import CdnDecrypter
|
from ..crypto import CdnDecrypter
|
||||||
from .tl.custom import InputSizedFile
|
from ..tl.custom import InputSizedFile
|
||||||
from .tl.functions.help import AcceptTermsOfServiceRequest
|
from ..tl.functions.help import AcceptTermsOfServiceRequest
|
||||||
from .tl.functions.updates import GetDifferenceRequest
|
from ..tl.functions.updates import GetDifferenceRequest
|
||||||
from .tl.functions.upload import (
|
from ..tl.functions.upload import (
|
||||||
SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest
|
SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest
|
||||||
)
|
)
|
||||||
from .tl.types.updates import (
|
from ..tl.types.updates import (
|
||||||
DifferenceSlice, DifferenceEmpty, Difference, DifferenceTooLong
|
DifferenceSlice, DifferenceEmpty, Difference, DifferenceTooLong
|
||||||
)
|
)
|
||||||
from .tl.types.upload import FileCdnRedirect
|
from ..tl.types.upload import FileCdnRedirect
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import socks
|
import socks
|
||||||
|
@ -35,23 +35,23 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
hachoir = None
|
hachoir = None
|
||||||
|
|
||||||
from . import TelegramBareClient
|
from .telegrambaseclient import TelegramBaseClient
|
||||||
from . import helpers, events
|
from .. import helpers, events
|
||||||
from .errors import (
|
from ..errors import (
|
||||||
PhoneCodeEmptyError, PhoneCodeExpiredError,
|
PhoneCodeEmptyError, PhoneCodeExpiredError,
|
||||||
PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError,
|
PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError,
|
||||||
SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError,
|
SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError,
|
||||||
PhoneNumberOccupiedError
|
PhoneNumberOccupiedError
|
||||||
)
|
)
|
||||||
from .tl.custom import Draft, Dialog
|
from ..tl.custom import Draft, Dialog
|
||||||
from .tl.functions.account import (
|
from ..tl.functions.account import (
|
||||||
GetPasswordRequest, UpdatePasswordSettingsRequest
|
GetPasswordRequest, UpdatePasswordSettingsRequest
|
||||||
)
|
)
|
||||||
from .tl.functions.auth import (
|
from ..tl.functions.auth import (
|
||||||
CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest,
|
CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest,
|
||||||
SignUpRequest, ResendCodeRequest, ImportBotAuthorizationRequest
|
SignUpRequest, ResendCodeRequest, ImportBotAuthorizationRequest
|
||||||
)
|
)
|
||||||
from .tl.functions.messages import (
|
from ..tl.functions.messages import (
|
||||||
GetDialogsRequest, GetHistoryRequest, SendMediaRequest,
|
GetDialogsRequest, GetHistoryRequest, SendMediaRequest,
|
||||||
SendMessageRequest, GetAllDraftsRequest,
|
SendMessageRequest, GetAllDraftsRequest,
|
||||||
ReadMentionsRequest, SendMultiMediaRequest,
|
ReadMentionsRequest, SendMultiMediaRequest,
|
||||||
|
@ -59,13 +59,13 @@ from .tl.functions.messages import (
|
||||||
ForwardMessagesRequest, SearchRequest
|
ForwardMessagesRequest, SearchRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
from .tl.functions import channels
|
from ..tl.functions import channels
|
||||||
from .tl.functions import messages
|
from ..tl.functions import messages
|
||||||
|
|
||||||
from .tl.functions.channels import (
|
from ..tl.functions.channels import (
|
||||||
GetFullChannelRequest, GetParticipantsRequest
|
GetFullChannelRequest, GetParticipantsRequest
|
||||||
)
|
)
|
||||||
from .tl.types import (
|
from ..tl.types import (
|
||||||
DocumentAttributeAudio, DocumentAttributeFilename,
|
DocumentAttributeAudio, DocumentAttributeFilename,
|
||||||
InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty,
|
InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty,
|
||||||
Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
|
Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
|
||||||
|
@ -81,21 +81,21 @@ from .tl.types import (
|
||||||
ChannelParticipantsBanned, ChannelParticipantsKicked,
|
ChannelParticipantsBanned, ChannelParticipantsKicked,
|
||||||
InputMessagesFilterEmpty, UpdatesCombined
|
InputMessagesFilterEmpty, UpdatesCombined
|
||||||
)
|
)
|
||||||
from .tl.types.messages import DialogsSlice, MessagesNotModified
|
from ..tl.types.messages import DialogsSlice, MessagesNotModified
|
||||||
from .tl.types.account import PasswordInputSettings, NoPassword
|
from ..tl.types.account import PasswordInputSettings, NoPassword
|
||||||
from .tl import custom
|
from ..tl import custom
|
||||||
from .utils import Default
|
from ..utils import Default
|
||||||
from .extensions import markdown, html
|
from ..extensions import markdown, html
|
||||||
|
|
||||||
__log__ = logging.getLogger(__name__)
|
__log__ = logging.getLogger(__name__)
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from . import utils
|
from .. import utils
|
||||||
from .errors import RPCError
|
from ..errors import RPCError
|
||||||
from .tl import TLObject
|
from ..tl import TLObject
|
||||||
|
|
||||||
|
|
||||||
class TelegramClient(TelegramBareClient):
|
class TelegramClient(TelegramBaseClient):
|
||||||
"""
|
"""
|
||||||
Initializes the Telegram client with the specified API ID and Hash. This
|
Initializes the Telegram client with the specified API ID and Hash. This
|
||||||
is identical to the `telethon.telegram_bare_client.TelegramBareClient`
|
is identical to the `telethon.telegram_bare_client.TelegramBareClient`
|
||||||
|
|
264
telethon/client/users.py
Normal file
264
telethon/client/users.py
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
import asyncio
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from .telegrambaseclient import TelegramBaseClient
|
||||||
|
from .. import errors, utils
|
||||||
|
from ..tl import TLObject, types, functions
|
||||||
|
|
||||||
|
|
||||||
|
class UserMethods(TelegramBaseClient):
|
||||||
|
async def __call__(self, request, retries=5, ordered=False):
|
||||||
|
requests = (request,) if not utils.is_list_like(request) else request
|
||||||
|
if not all(isinstance(x, TLObject) and
|
||||||
|
x.content_related for x in requests):
|
||||||
|
raise TypeError('You can only invoke requests, not types!')
|
||||||
|
|
||||||
|
for r in requests:
|
||||||
|
await r.resolve(self, utils)
|
||||||
|
|
||||||
|
for _ in range(retries):
|
||||||
|
try:
|
||||||
|
future = self._sender.send(request, ordered=ordered)
|
||||||
|
if isinstance(future, list):
|
||||||
|
results = []
|
||||||
|
for f in future:
|
||||||
|
results.append(await f)
|
||||||
|
return results
|
||||||
|
else:
|
||||||
|
return await future
|
||||||
|
except (errors.ServerError, errors.RpcCallFailError):
|
||||||
|
pass
|
||||||
|
except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e:
|
||||||
|
if e.seconds <= self.session.flood_sleep_threshold:
|
||||||
|
await asyncio.sleep(e.seconds)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
|
||||||
|
errors.UserMigrateError) as e:
|
||||||
|
await self._switch_dc(e.new_dc)
|
||||||
|
|
||||||
|
raise ValueError('Number of retries reached 0')
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
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 get_entity(self, entity):
|
||||||
|
"""
|
||||||
|
Turns the given entity into a valid Telegram user or chat.
|
||||||
|
|
||||||
|
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
|
||||||
|
The entity (or iterable of entities) to be transformed.
|
||||||
|
If it's a string which can be converted to an integer or starts
|
||||||
|
with '+' it will be resolved as if it were a phone number.
|
||||||
|
|
||||||
|
If it doesn't start with '+' or starts with a '@' it will be
|
||||||
|
be resolved from the username. If no exact match is returned,
|
||||||
|
an error will be raised.
|
||||||
|
|
||||||
|
If the entity is an integer or a Peer, its information will be
|
||||||
|
returned through a call to self.get_input_peer(entity).
|
||||||
|
|
||||||
|
If the entity is neither, and it's not a TLObject, an
|
||||||
|
error 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 = [
|
||||||
|
x if isinstance(x, str) else await self.get_input_entity(x)
|
||||||
|
for x in entity
|
||||||
|
]
|
||||||
|
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 = [
|
||||||
|
await self._get_entity_from_string(x) if isinstance(x, str)
|
||||||
|
else (
|
||||||
|
id_entity[utils.get_peer_id(x)]
|
||||||
|
if not isinstance(x, types.InputPeerSelf)
|
||||||
|
else next(u for u in id_entity.values()
|
||||||
|
if isinstance(u, types.User) and u.is_self)
|
||||||
|
)
|
||||||
|
for x in inputs
|
||||||
|
]
|
||||||
|
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 InputUser, InputChat and so on, so this is the
|
||||||
|
most suitable call to make for those cases.
|
||||||
|
|
||||||
|
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
|
||||||
|
The integer ID of an user or otherwise either of a
|
||||||
|
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for
|
||||||
|
which to get its ``Input*`` version.
|
||||||
|
|
||||||
|
If this ``Peer`` hasn't been seen before by the library, the top
|
||||||
|
dialogs will be loaded and their entities saved to the session
|
||||||
|
file (unless this feature was disabled explicitly).
|
||||||
|
|
||||||
|
If in the end the access hash required for the peer was not found,
|
||||||
|
a 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)
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
'Could not find the input entity for "{}". Please read https://'
|
||||||
|
'telethon.readthedocs.io/en/latest/extra/basic/entities.html to'
|
||||||
|
' find out more details.'
|
||||||
|
.format(peer)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
an 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:
|
||||||
|
for user in (await self(
|
||||||
|
functions.contacts.GetContactsRequest(0))).users:
|
||||||
|
if user.phone == phone:
|
||||||
|
return user
|
||||||
|
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:
|
||||||
|
if username in ('me', 'self'):
|
||||||
|
return await self.get_me()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self(
|
||||||
|
functions.contacts.ResolveUsernameRequest(username))
|
||||||
|
except errors.UsernameNotOccupiedError as e:
|
||||||
|
raise ValueError('No user has "{}" as username'
|
||||||
|
.format(username)) from e
|
||||||
|
|
||||||
|
for entity in itertools.chain(result.users, result.chats):
|
||||||
|
if getattr(entity, 'username', None) or '' \
|
||||||
|
.lower() == username:
|
||||||
|
return entity
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
# endregion
|
Loading…
Reference in New Issue
Block a user