2
0
mirror of https://github.com/pyrogram/pyrogram synced 2025-09-08 10:15:10 +00:00

Merge branch 'layer-104' into develop

This commit is contained in:
Dan
2019-09-07 12:47:25 +02:00
13 changed files with 824 additions and 151 deletions

View File

@@ -18,7 +18,6 @@
import logging
import math
import mimetypes
import os
import re
import shutil
@@ -45,12 +44,13 @@ from pyrogram.errors import (
PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded,
PasswordHashInvalid, FloodWait, PeerIdInvalid, FirstnameInvalid, PhoneNumberBanned,
VolumeLocNotFound, UserMigrate, ChannelPrivate, PhoneNumberOccupied,
PasswordRecoveryNa, PasswordEmpty, AuthBytesInvalid
)
PasswordRecoveryNa, PasswordEmpty, AuthBytesInvalid,
BadRequest)
from pyrogram.session import Auth, Session
from .ext import utils, Syncer, BaseClient, Dispatcher
from .methods import Methods
from .storage import Storage, FileStorage, MemoryStorage
from .types import User, SentCode, TermsOfService
log = logging.getLogger(__name__)
@@ -188,8 +188,6 @@ class Client(Methods, BaseClient):
"""
terms_of_service_displayed = False
def __init__(
self,
session_name: Union[str, Storage],
@@ -280,70 +278,64 @@ class Client(Methods, BaseClient):
self._proxy["enabled"] = bool(value.get("enabled", True))
self._proxy.update(value)
def start(self):
"""Start the client.
def connect(self) -> bool:
"""
Connect the client to Telegram servers.
This method connects the client to Telegram and, in case of new sessions, automatically manages the full login
process using an interactive prompt (by default).
Has no parameters.
Returns:
``bool``: On success, in case the passed-in session is authorized, True is returned. Otherwise, in case
the session needs to be authorized, False is returned.
Raises:
ConnectionError: In case you try to start an already started client.
Example:
.. code-block:: python
:emphasize-lines: 4
from pyrogram import Client
app = Client("my_account")
app.start()
... # Call API methods
app.stop()
ConnectionError: In case you try to connect an already connected client.
"""
if self.is_started:
raise ConnectionError("Client has already been started")
if self.is_connected:
raise ConnectionError("Client is already connected")
self.load_config()
self.load_session()
self.load_plugins()
self.session = Session(self, self.storage.dc_id, self.storage.auth_key)
self.session.start()
self.is_started = True
try:
if self.storage.user_id is None:
if self.bot_token is None:
self.storage.is_bot = False
self.authorize_user()
else:
self.storage.is_bot = True
self.authorize_bot()
self.is_connected = True
if not self.storage.is_bot:
if self.takeout:
self.takeout_id = self.send(functions.account.InitTakeoutSession()).id
log.warning("Takeout session {} initiated".format(self.takeout_id))
return bool(self.storage.user_id)
now = time.time()
def disconnect(self):
"""Disconnect the client from Telegram servers.
if abs(now - self.storage.date) > Client.OFFLINE_SLEEP:
self.get_initial_dialogs()
self.get_contacts()
else:
self.send(functions.messages.GetPinnedDialogs(folder_id=0))
self.get_initial_dialogs_chunk()
else:
self.send(functions.updates.GetState())
except Exception as e:
self.is_started = False
self.session.stop()
raise e
Raises:
ConnectionError: In case you try to disconnect an already disconnected client or in case you try to
disconnect a client that needs to be terminated first.
"""
if not self.is_connected:
raise ConnectionError("Client is already disconnected")
if self.is_initialized:
raise ConnectionError("Can't disconnect an initialized client")
self.session.stop()
self.storage.close()
self.is_connected = False
def initialize(self):
"""Initialize the client by starting up workers.
This method will start updates and download workers.
It will also load plugins and start the internal dispatcher.
Raises:
ConnectionError: In case you try to initialize a disconnected client or in case you try to initialize an
already initialized client.
"""
if not self.is_connected:
raise ConnectionError("Can't initialize a disconnected client")
if self.is_initialized:
raise ConnectionError("Client is already initialized")
self.load_plugins()
for i in range(self.UPDATES_WORKERS):
self.updates_workers_list.append(
@@ -367,36 +359,21 @@ class Client(Methods, BaseClient):
self.dispatcher.start()
mimetypes.init()
Syncer.add(self)
return self
self.is_initialized = True
def stop(self):
"""Stop the Client.
def terminate(self):
"""Terminate the client by shutting down workers.
This method disconnects the client from Telegram and stops the underlying tasks.
Has no parameters.
This method does the opposite of :meth:`~Client.initialize`.
It will stop the dispatcher and shut down updates and download workers.
Raises:
ConnectionError: In case you try to stop an already stopped client.
Example:
.. code-block:: python
:emphasize-lines: 8
from pyrogram import Client
app = Client("my_account")
app.start()
... # Call API methods
app.stop()
ConnectionError: In case you try to terminate a client that is already terminated.
"""
if not self.is_started:
raise ConnectionError("Client is already stopped")
if not self.is_initialized:
raise ConnectionError("Client is already terminated")
if self.takeout_id:
self.send(functions.account.FinishTakeoutSession())
@@ -426,8 +403,490 @@ class Client(Methods, BaseClient):
self.media_sessions.clear()
self.is_started = False
self.session.stop()
self.is_initialized = False
def send_code(self, phone_number: str) -> SentCode:
"""Send the confirmation code to the given phone number.
Parameters:
phone_number (``str``):
Phone number in international format (includes the country prefix).
Returns:
:obj:`SentCode`: On success, an object containing information on the sent confirmation code is returned.
Raises:
BadRequest: In case the phone number is invalid.
"""
phone_number = phone_number.strip(" +")
while True:
try:
r = self.send(
functions.auth.SendCode(
phone_number=phone_number,
api_id=self.api_id,
api_hash=self.api_hash,
settings=types.CodeSettings()
)
)
except (PhoneMigrate, NetworkMigrate) as e:
self.session.stop()
self.storage.dc_id = e.x
self.storage.auth_key = Auth(self, self.storage.dc_id).create()
self.session = Session(self, self.storage.dc_id, self.storage.auth_key)
self.session.start()
else:
return SentCode._parse(r)
def resend_code(self, phone_number: str, phone_code_hash: str) -> SentCode:
"""Re-send the confirmation code using a different type.
The type of the code to be re-sent is specified in the *next_type* attribute of the :obj:`SentCode` object
returned by :meth:`send_code`.
Parameters:
phone_number (``str``):
Phone number in international format (includes the country prefix).
phone_code_hash (``str``):
Confirmation code identifier.
Returns:
:obj:`SentCode`: On success, an object containing information on the re-sent confirmation code is returned.
Raises:
BadRequest: In case the arguments are invalid.
"""
phone_number = phone_number.strip(" +")
r = self.send(
functions.auth.ResendCode(
phone_number=phone_number,
phone_code_hash=phone_code_hash
)
)
return SentCode._parse(r)
def sign_in(self, phone_number: str, phone_code_hash: str, phone_code: str) -> Union[User, TermsOfService, bool]:
"""Authorize a user in Telegram with a valid confirmation code.
Parameters:
phone_number (``str``):
Phone number in international format (includes the country prefix).
phone_code_hash (``str``):
Code identifier taken from the result of :meth:`~Client.send_code`.
phone_code (``str``):
The valid confirmation code you received (either as Telegram message or as SMS in your phone number).
Returns:
:obj:`User` | :obj:`TermsOfService` | bool: On success, in case the authorization completed, the user is
returned. In case the phone number needs to be registered first AND the terms of services accepted (with
:meth:`~Client.accept_terms_of_service`), an object containing them is returned. In case the phone number
needs to be registered, but the terms of services don't need to be accepted, False is returned instead.
Raises:
BadRequest: In case the arguments are invalid.
SessionPasswordNeeded: In case a password is needed to sign in.
"""
phone_number = phone_number.strip(" +")
r = self.send(
functions.auth.SignIn(
phone_number=phone_number,
phone_code_hash=phone_code_hash,
phone_code=phone_code
)
)
if isinstance(r, types.auth.AuthorizationSignUpRequired):
if r.terms_of_service:
return TermsOfService._parse(terms_of_service=r.terms_of_service)
return False
else:
self.storage.user_id = r.user.id
self.storage.is_bot = False
return User._parse(self, r.user)
def sign_up(self, phone_number: str, phone_code_hash: str, first_name: str, last_name: str = "") -> User:
"""Register a new user in Telegram.
Parameters:
phone_number (``str``):
Phone number in international format (includes the country prefix).
phone_code_hash (``str``):
Code identifier taken from the result of :meth:`~Client.send_code`.
first_name (``str``):
New user first name.
last_name (``str``, *optional*):
New user last name. Defaults to "" (empty string).
Returns:
:obj:`User`: On success, the new registered user is returned.
Raises:
BadRequest: In case the arguments are invalid.
"""
phone_number = phone_number.strip(" +")
r = self.send(
functions.auth.SignUp(
phone_number=phone_number,
first_name=first_name,
last_name=last_name,
phone_code_hash=phone_code_hash
)
)
self.storage.user_id = r.user.id
self.storage.is_bot = False
return User._parse(self, r.user)
def sign_in_bot(self, bot_token: str) -> User:
"""Authorize a bot using its bot token generated by BotFather.
Parameters:
bot_token (``str``):
The bot token generated by BotFather
Returns:
:obj:`User`: On success, the bot identity is return in form of a user object.
Raises:
BadRequest: In case the bot token is invalid.
"""
while True:
try:
r = self.send(
functions.auth.ImportBotAuthorization(
flags=0,
api_id=self.api_id,
api_hash=self.api_hash,
bot_auth_token=bot_token
)
)
except UserMigrate as e:
self.session.stop()
self.storage.dc_id = e.x
self.storage.auth_key = Auth(self, self.storage.dc_id).create()
self.session = Session(self, self.storage.dc_id, self.storage.auth_key)
self.session.start()
else:
self.storage.user_id = r.user.id
self.storage.is_bot = True
return User._parse(self, r.user)
def get_password_hint(self) -> str:
"""Get your Two-Step Verification password hint.
Returns:
``str``: On success, the password hint as string is returned.
"""
return self.send(functions.account.GetPassword()).hint
def check_password(self, password: str) -> User:
"""Check your Two-Step Verification password and log in.
Parameters:
password (``str``):
Your Two-Step Verification password.
Returns:
:obj:`User`: On success, the authorized user is returned.
Raises:
BadRequest: In case the password is invalid.
"""
r = self.send(
functions.auth.CheckPassword(
password=compute_check(
self.send(functions.account.GetPassword()),
password
)
)
)
self.storage.user_id = r.user.id
self.storage.is_bot = False
return User._parse(self, r.user)
def send_recovery_code(self) -> str:
"""Send a code to your email to recover your password.
Returns:
``str``: On success, the hidden email pattern is returned and a recovery code is sent to that email.
Raises:
BadRequest: In case no recovery email was set up.
"""
return self.send(
functions.auth.RequestPasswordRecovery()
).email_pattern
def recover_password(self, recovery_code: str) -> User:
"""Recover your password with a recovery code and log in.
Parameters:
recovery_code (``str``):
The recovery code sent via email.
Returns:
:obj:`User`: On success, the authorized user is returned and the Two-Step Verification password reset.
Raises:
BadRequest: In case the recovery code is invalid.
"""
r = self.send(
functions.auth.RecoverPassword(
code=recovery_code
)
)
self.storage.user_id = r.user.id
self.storage.is_bot = False
return User._parse(self, r.user)
def accept_terms_of_service(self, terms_of_service_id: str) -> bool:
"""Accept the given terms of service.
Parameters:
terms_of_service_id (``str``):
The terms of service identifier.
"""
r = self.send(
functions.help.AcceptTermsOfService(
id=types.DataJSON(
data=terms_of_service_id
)
)
)
assert r
return True
def authorize(self) -> User:
if self.bot_token is not None:
return self.sign_in_bot(self.bot_token)
while True:
if self.phone_number is None:
while True:
value = input("Enter phone number or bot token: ")
confirm = input("Is \"{}\" correct? (y/n): ".format(value))
if confirm in ("y", "1"):
break
elif confirm in ("n", "2"):
continue
if ":" in value:
self.bot_token = value
return self.sign_in_bot(value)
else:
self.phone_number = value
try:
sent_code = self.send_code(self.phone_number)
except BadRequest as e:
print(e.MESSAGE)
self.phone_number = None
except FloodWait as e:
print(e.MESSAGE.format(x=e.x))
time.sleep(e.x)
except Exception as e:
log.error(e, exc_info=True)
else:
break
if self.force_sms:
sent_code = self.resend_code(self.phone_number, sent_code.phone_code_hash)
print("The confirmation code has been sent via {}".format(
{
"app": "Telegram app",
"sms": "SMS",
"call": "phone call",
"flash_call": "phone flash call"
}[sent_code.type]
))
while True:
if self.phone_code is None:
self.phone_code = input("Enter confirmation code: ")
try:
signed_in = self.sign_in(self.phone_number, sent_code.phone_code_hash, self.phone_code)
except BadRequest as e:
print(e.MESSAGE)
self.phone_code = None
except SessionPasswordNeeded as e:
print(e.MESSAGE)
while True:
print("Password hint: {}".format(self.get_password_hint()))
if self.password is None:
self.password = input("Enter password (empty to recover): ")
try:
if self.password == "":
confirm = input("Confirm password recovery (y/n): ")
if confirm in ("y", "1"):
email_pattern = self.send_recovery_code()
print("The recovery code has been sent to {}".format(email_pattern))
while True:
recovery_code = input("Enter recovery code: ")
try:
return self.recover_password(recovery_code)
except BadRequest as e:
print(e.MESSAGE)
except FloodWait as e:
print(e.MESSAGE.format(x=e.x))
time.sleep(e.x)
except Exception as e:
log.error(e, exc_info=True)
raise
elif confirm in ("n", "2"):
self.password = None
else:
return self.check_password(self.password)
except BadRequest as e:
print(e.MESSAGE)
self.password = None
except FloodWait as e:
print(e.MESSAGE.format(x=e.x))
time.sleep(e.x)
except Exception as e:
log.error(e, exc_info=True)
raise
except FloodWait as e:
print(e.MESSAGE.format(x=e.x))
time.sleep(e.x)
except Exception as e:
log.error(e, exc_info=True)
else:
break
if isinstance(signed_in, User):
return signed_in
while True:
self.first_name = input("Enter first name: ")
self.last_name = input("Enter last name (empty to skip): ")
try:
signed_up = self.sign_up(
self.phone_number,
sent_code.phone_code_hash,
self.first_name,
self.last_name
)
except BadRequest as e:
print(e.MESSAGE)
self.first_name = None
self.last_name = None
except FloodWait as e:
print(e.MESSAGE.format(x=e.x))
time.sleep(e.x)
else:
break
if isinstance(signed_in, TermsOfService):
print("\n" + signed_in.text + "\n")
self.accept_terms_of_service(signed_in.id)
return signed_up
def start(self):
"""Start the client.
This method connects the client to Telegram and, in case of new sessions, automatically manages the full
authorization process using an interactive prompt.
Returns:
:obj:`Client`: The started client itself.
Raises:
ConnectionError: In case you try to start an already started client.
Example:
.. code-block:: python
:emphasize-lines: 4
from pyrogram import Client
app = Client("my_account")
app.start()
... # Call API methods
app.stop()
"""
is_authorized = self.connect()
try:
if not is_authorized:
self.authorize()
if not self.storage.is_bot and self.takeout:
self.takeout_id = self.send(functions.account.InitTakeoutSession()).id
log.warning("Takeout session {} initiated".format(self.takeout_id))
self.send(functions.updates.GetState())
except Exception as e:
self.disconnect()
raise e
else:
self.initialize()
return self
def stop(self):
"""Stop the Client.
This method disconnects the client from Telegram and stops the underlying tasks.
Returns:
:obj:`Client`: The stopped client itself.
Raises:
ConnectionError: In case you try to stop an already stopped client.
Example:
.. code-block:: python
:emphasize-lines: 8
from pyrogram import Client
app = Client("my_account")
app.start()
... # Call API methods
app.stop()
"""
self.terminate()
self.disconnect()
return self
@@ -437,7 +896,8 @@ class Client(Methods, BaseClient):
This method will first call :meth:`~Client.stop` and then :meth:`~Client.start` in a row in order to restart
a client using a single method.
Has no parameters.
Returns:
:obj:`Client`: The restarted client itself.
Raises:
ConnectionError: In case you try to restart a stopped Client.
@@ -462,6 +922,8 @@ class Client(Methods, BaseClient):
self.stop()
self.start()
return self
@staticmethod
def idle(stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)):
"""Block the main script execution until a signal is received.
@@ -525,8 +987,6 @@ class Client(Methods, BaseClient):
sequence. It makes running a client less verbose, but is not suitable in case you want to run more than one
client in a single main script, since idle() will block after starting the own client.
Has no parameters.
Raises:
ConnectionError: In case you try to run an already started client.
@@ -628,8 +1088,6 @@ class Client(Methods, BaseClient):
This method must be called inside a progress callback function in order to stop the transmission at the
desired time. The progress callback is called every time a file chunk is uploaded/downloaded.
Has no parameters.
Example:
.. code-block:: python
:emphasize-lines: 9
@@ -656,8 +1114,6 @@ class Client(Methods, BaseClient):
More detailed information about session strings can be found at the dedicated page of
:doc:`Storage Engines <../../topics/storage-engines>`.
Has no parameters.
Returns:
``str``: The session serialized into a printable, url-safe string.
@@ -1211,8 +1667,8 @@ class Client(Methods, BaseClient):
Raises:
RPCError: In case of a Telegram RPC error.
"""
if not self.is_started:
raise ConnectionError("Client has not been started")
if not self.is_connected:
raise ConnectionError("Client has not been started yet")
if self.no_updates:
data = functions.InvokeWithoutUpdates(query=data)
@@ -1444,37 +1900,37 @@ class Client(Methods, BaseClient):
log.warning('[{}] No plugin loaded from "{}"'.format(
self.session_name, root))
def get_initial_dialogs_chunk(self, offset_date: int = 0):
while True:
try:
r = self.send(
functions.messages.GetDialogs(
offset_date=offset_date,
offset_id=0,
offset_peer=types.InputPeerEmpty(),
limit=self.DIALOGS_AT_ONCE,
hash=0,
exclude_pinned=True
)
)
except FloodWait as e:
log.warning("get_dialogs flood: waiting {} seconds".format(e.x))
time.sleep(e.x)
else:
log.info("Total peers: {}".format(self.storage.peers_count))
return r
def get_initial_dialogs(self):
self.send(functions.messages.GetPinnedDialogs(folder_id=0))
dialogs = self.get_initial_dialogs_chunk()
offset_date = utils.get_offset_date(dialogs)
while len(dialogs.dialogs) == self.DIALOGS_AT_ONCE:
dialogs = self.get_initial_dialogs_chunk(offset_date)
offset_date = utils.get_offset_date(dialogs)
self.get_initial_dialogs_chunk()
# def get_initial_dialogs_chunk(self, offset_date: int = 0):
# while True:
# try:
# r = self.send(
# functions.messages.GetDialogs(
# offset_date=offset_date,
# offset_id=0,
# offset_peer=types.InputPeerEmpty(),
# limit=self.DIALOGS_AT_ONCE,
# hash=0,
# exclude_pinned=True
# )
# )
# except FloodWait as e:
# log.warning("get_dialogs flood: waiting {} seconds".format(e.x))
# time.sleep(e.x)
# else:
# log.info("Total peers: {}".format(self.storage.peers_count))
# return r
#
# def get_initial_dialogs(self):
# self.send(functions.messages.GetPinnedDialogs(folder_id=0))
#
# dialogs = self.get_initial_dialogs_chunk()
# offset_date = utils.get_offset_date(dialogs)
#
# while len(dialogs.dialogs) == self.DIALOGS_AT_ONCE:
# dialogs = self.get_initial_dialogs_chunk(offset_date)
# offset_date = utils.get_offset_date(dialogs)
#
# self.get_initial_dialogs_chunk()
def resolve_peer(self, peer_id: Union[int, str]):
"""Get the InputPeer of a known peer id.
@@ -1495,9 +1951,11 @@ class Client(Methods, BaseClient):
``InputPeer``: On success, the resolved peer id is returned in form of an InputPeer object.
Raises:
RPCError: In case of a Telegram RPC error.
KeyError: In case the peer doesn't exist in the internal database.
"""
if not self.is_connected:
raise ConnectionError("Client has not been started yet")
try:
return self.storage.get_peer_by_id(peer_id)
except KeyError:
@@ -1678,7 +2136,7 @@ class Client(Methods, BaseClient):
file_part += 1
if progress:
progress(self, min(file_part * part_size, file_size), file_size, *progress_args)
progress(min(file_part * part_size, file_size), file_size, *progress_args)
except Client.StopTransmission:
raise
except Exception as e:
@@ -1813,7 +2271,6 @@ class Client(Methods, BaseClient):
if progress:
progress(
self,
min(offset, file_size)
if file_size != 0
else offset,
@@ -1896,7 +2353,6 @@ class Client(Methods, BaseClient):
if progress:
progress(
self,
min(offset, file_size)
if file_size != 0
else offset,