diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index a54519d6..6e90025a 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -2,6 +2,7 @@ import asyncio import itertools import logging import platform +import warnings from datetime import timedelta, datetime from . import version, errors, utils @@ -418,7 +419,10 @@ class TelegramBareClient: raise ValueError('Number of retries reached 0') # Let people use client.invoke(SomeRequest()) instead client(...) - invoke = __call__ + async def invoke(self, *args, **kwargs): + warnings.warn('client.invoke(...) is deprecated, ' + 'use client(...) instead') + return await self(*args, **kwargs) # endregion diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index fb4f07ef..6f7b77ff 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -3,18 +3,15 @@ import hashlib import io import itertools import logging -import os import re import sys import time import warnings from collections import UserList -from datetime import datetime, timedelta from io import BytesIO from mimetypes import guess_type from .crypto import CdnDecrypter -from .tl import TLObject from .tl.custom import InputSizedFile from .tl.functions.help import AcceptTermsOfServiceRequest from .tl.functions.updates import GetDifferenceRequest @@ -39,14 +36,13 @@ except ImportError: hachoir = None from . import TelegramBareClient -from . import helpers, utils, events +from . import helpers, events from .errors import ( - RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, + PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError, - PhoneNumberOccupiedError, UsernameNotOccupiedError + PhoneNumberOccupiedError ) -from .network import ConnectionTcpFull from .tl.custom import Draft, Dialog from .tl.functions.account import ( GetPasswordRequest, UpdatePasswordSettingsRequest @@ -55,13 +51,10 @@ from .tl.functions.auth import ( CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest, SignUpRequest, ResendCodeRequest, ImportBotAuthorizationRequest ) -from .tl.functions.contacts import ( - GetContactsRequest, ResolveUsernameRequest -) from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, - SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, - CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, + SendMessageRequest, GetAllDraftsRequest, + ReadMentionsRequest, SendMultiMediaRequest, UploadMediaRequest, EditMessageRequest, GetFullChatRequest, ForwardMessagesRequest, SearchRequest ) @@ -69,26 +62,22 @@ from .tl.functions.messages import ( from .tl.functions import channels from .tl.functions import messages -from .tl.functions.users import ( - GetUsersRequest -) from .tl.functions.channels import ( - GetChannelsRequest, GetFullChannelRequest, GetParticipantsRequest + GetFullChannelRequest, GetParticipantsRequest ) from .tl.types import ( DocumentAttributeAudio, DocumentAttributeFilename, InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty, Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, - InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, + UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, - PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, - ChatInvite, ChatInviteAlready, Photo, InputPeerSelf, - InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, + PeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, + Photo, InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, InputMessageEntityMentionName, DocumentAttributeVideo, UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize, - PhotoSizeEmpty, MessageService, ChatParticipants, User, WebPage, + PhotoSizeEmpty, MessageService, ChatParticipants, WebPage, ChannelParticipantsBanned, ChannelParticipantsKicked, InputMessagesFilterEmpty, UpdatesCombined ) @@ -99,123 +88,21 @@ from .utils import Default from .extensions import markdown, html __log__ = logging.getLogger(__name__) +import os +from datetime import datetime +from . import utils +from .errors import RPCError +from .tl import TLObject class TelegramClient(TelegramBareClient): """ - Initializes the Telegram client with the specified API ID and Hash. - - Args: - session (`str` | `telethon.sessions.abstract.Session`, `None`): - The file name of the session file to be used if a string is - given (it may be a full path), or the Session instance to be - used otherwise. If it's ``None``, the session will not be saved, - and you should call :meth:`.log_out()` when you're done. - - Note that if you pass a string it will be a file in the current - working directory, although you can also pass absolute paths. - - The session file contains enough information for you to login - without re-sending the code, so if you have to enter the code - more than once, maybe you're changing the working directory, - renaming or removing the file, or using random names. - - api_id (`int` | `str`): - The API ID you obtained from https://my.telegram.org. - - api_hash (`str`): - The API ID you obtained from https://my.telegram.org. - - connection (`telethon.network.connection.common.Connection`, optional): - The connection instance to be used when creating a new connection - to the servers. If it's a type, the `proxy` argument will be used. - - Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`. - - use_ipv6 (`bool`, optional): - Whether to connect to the servers through IPv6 or not. - By default this is ``False`` as IPv6 support is not - too widespread yet. - - proxy (`tuple` | `dict`, optional): - A tuple consisting of ``(socks.SOCKS5, 'host', port)``. - See https://github.com/Anorov/PySocks#usage-1 for more. - - update_workers (`int`, optional): - If specified, represents how many extra threads should - be spawned to handle incoming updates, and updates will - be kept in memory until they are processed. Note that - you must set this to at least ``0`` if you want to be - able to process updates through :meth:`updates.poll()`. - - timeout (`int` | `float` | `timedelta`, optional): - The timeout to be used when receiving responses from - the network. Defaults to 5 seconds. - - spawn_read_thread (`bool`, optional): - Whether to use an extra background thread or not. Defaults - to ``True`` so receiving items from the network happens - instantly, as soon as they arrive. Can still be disabled - if you want to run the library without any additional thread. - - report_errors (`bool`, optional): - Whether to report RPC errors or not. Defaults to ``True``, - see :ref:`api-status` for more information. - - Kwargs: - Some extra parameters are required when establishing the first - connection. These are are (along with their default values): - - .. code-block:: python - - device_model = platform.node() - system_version = platform.system() - app_version = TelegramClient.__version__ - lang_code = 'en' - system_lang_code = lang_code + Initializes the Telegram client with the specified API ID and Hash. This + is identical to the `telethon.telegram_bare_client.TelegramBareClient` + but it contains "friendly methods", so please refer to its documentation + to know what parameters you can use when creating a new instance. """ - # region Initialization - - def __init__(self, session, api_id, api_hash, - *, - connection=ConnectionTcpFull, - use_ipv6=False, - proxy=None, - update_workers=None, - timeout=timedelta(seconds=10), - spawn_read_thread=True, - report_errors=True, - **kwargs): - super().__init__( - session, api_id, api_hash, - connection=connection, - use_ipv6=use_ipv6, - proxy=proxy, - update_workers=update_workers, - spawn_read_thread=spawn_read_thread, - timeout=timeout, - report_errors=report_errors, - **kwargs - ) - - self._event_builders = [] - self._events_pending_resolve = [] - - # Default parse mode - self._parse_mode = markdown - - # Some fields to easy signing in. Let {phone: hash} be - # a dictionary because the user may change their mind. - self._phone_code_hash = {} - self._phone = None - self._tos = None - - # Sometimes we need to know who we are, cache the self peer - self._self_input_peer = None - - # endregion - # region Telegram requests functions # region Authorization requests @@ -540,34 +427,6 @@ class TelegramClient(TelegramBareClient): self._authorized = False return True - 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 = self(GetUsersRequest([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 UnauthorizedError: - return None - # endregion # region Dialogs ("chats") requests @@ -2648,182 +2507,6 @@ class TelegramClient(TelegramBareClient): super()._set_connected_and_authorized() self._check_events_pending_resolve() - 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 self.get_input_entity(x) - for x in entity - ] - users = [x for x in inputs - if isinstance(x, (InputPeerUser, InputPeerSelf))] - chats = [x.chat_id for x in inputs if isinstance(x, InputPeerChat)] - channels = [x for x in inputs if isinstance(x, InputPeerChannel)] - if users: - # GetUsersRequest has a limit of 200 per call - tmp = [] - while users: - curr, users = users[:200], users[200:] - tmp.extend(self(GetUsersRequest(curr))) - users = tmp - if chats: # TODO Handle chats slice? - chats = self(GetChatsRequest(chats)).chats - if channels: - channels = self(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 = [ - self._get_entity_from_string(x) if isinstance(x, str) - else ( - id_entity[utils.get_peer_id(x)] - if not isinstance(x, InputPeerSelf) - else next(u for u in id_entity.values() - if isinstance(u, User) and u.is_self) - ) - for x in inputs - ] - return result[0] if single else result - - 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 self(GetContactsRequest(0)).users: - if user.phone == phone: - return user - else: - username, is_join_chat = utils.parse_username(string) - if is_join_chat: - invite = self(CheckChatInviteRequest(username)) - if isinstance(invite, 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, ChatInviteAlready): - return invite.chat - elif username: - if username in ('me', 'self'): - return self.get_me() - - try: - result = self(ResolveUsernameRequest(username)) - except 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 self.get_entity(self.session.get_input_entity(string)) - except ValueError: - pass - - raise ValueError( - 'Cannot find any entity corresponding to "{}"'.format(string) - ) - - 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 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(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) - ) - def edit_2fa(self, current_password=None, new_password=None, hint='', email=None): """