diff --git a/MANIFEST.in b/MANIFEST.in index f818e13a..a1d19d94 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ ## Include -include COPYING COPYING.lesser NOTICE requirements.txt +include COPYING COPYING.lesser NOTICE requirements.txt requirements_extras.txt recursive-include compiler *.py *.tl *.tsv *.txt ## Exclude diff --git a/pyrogram/api/errors/error.py b/pyrogram/api/errors/error.py index aefbbca9..397af546 100644 --- a/pyrogram/api/errors/error.py +++ b/pyrogram/api/errors/error.py @@ -33,7 +33,11 @@ class Error(Exception): MESSAGE = None def __init__(self, x: int or RpcError = None, query_type: type = None): - super().__init__("[{} {}]: {}".format(self.CODE, self.ID or self.NAME, self.MESSAGE.format(x=x))) + super().__init__("[{} {}]: {}".format( + self.CODE, + self.ID or self.NAME, + str(self) or self.MESSAGE.format(x=x) + )) try: self.x = int(x) @@ -50,13 +54,13 @@ class Error(Exception): code = rpc_error.error_code if code not in exceptions: - raise UnknownError(rpc_error, query_type) + raise UnknownError(x=rpc_error, query_type=query_type) message = rpc_error.error_message id = re.sub(r"_\d+", "_X", message) if id not in exceptions[code]: - raise UnknownError(rpc_error, query_type) + raise UnknownError(x=rpc_error, query_type=query_type) x = re.search(r"_(\d+)", message) x = x.group(1) if x is not None else x @@ -64,7 +68,7 @@ class Error(Exception): raise getattr( import_module("pyrogram.api.errors"), exceptions[code][id] - )(x) + )(x=x) class UnknownError(Error): diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 98cf5e65..4d885e92 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -54,12 +54,6 @@ from .style import Markdown, HTML log = logging.getLogger(__name__) -class APIKey: - def __init__(self, api_id: int, api_hash: str): - self.api_id = api_id - self.api_hash = api_hash - - class Proxy: def __init__(self, enabled: bool, hostname: str, port: int, username: str, password: str): self.enabled = enabled @@ -81,10 +75,13 @@ class Client: For Bots: pass your Bot API token, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" Note: as long as a valid User session file exists, Pyrogram won't ask you again to input your phone number. - api_key (``tuple``, optional): - Your Telegram API Key as tuple: *(api_id, api_hash)*. - E.g.: *(12345, "0123456789abcdef0123456789abcdef")*. This is an alternative way to pass it if you - don't want to use the *config.ini* file. + api_id (``int``, optional): + The *api_id* part of your Telegram API Key, as integer. E.g.: 12345 + This is an alternative way to pass it if you don't want to use the *config.ini* file. + + api_hash (``str``, optional): + The *api_hash* part of your Telegram API Key, as string. E.g.: "0123456789abcdef0123456789abcdef" + This is an alternative way to pass it if you don't want to use the *config.ini* file. proxy (``dict``, optional): Your SOCKS5 Proxy settings as dict, @@ -110,13 +107,18 @@ class Client: Pass your Two-Step Verification password (if you have one) to avoid entering it manually. Only applicable for new sessions. + force_sms (``str``, optional): + Pass True to force Telegram sending the authorization code via SMS. + Only applicable for new sessions. + first_name (``str``, optional): Pass a First Name to avoid entering it manually. It will be used to automatically create a new Telegram account in case the phone number you passed is not registered yet. + Only applicable for new sessions. last_name (``str``, optional): Same purpose as *first_name*; pass a Last Name to avoid entering it manually. It can - be an empty string: "" + be an empty string: "". Only applicable for new sessions. workers (``int``, optional): Thread pool size for handling incoming updates. Defaults to 4. @@ -130,17 +132,20 @@ class Client: def __init__(self, session_name: str, - api_key: tuple or APIKey = None, + api_id: int or str = None, + api_hash: str = None, proxy: dict or Proxy = None, test_mode: bool = False, phone_number: str = None, phone_code: str or callable = None, password: str = None, + force_sms: bool = False, first_name: str = None, last_name: str = None, workers: int = 4): self.session_name = session_name - self.api_key = api_key + self.api_id = int(api_id) if api_id else None + self.api_hash = api_hash self.proxy = proxy self.test_mode = test_mode @@ -149,6 +154,7 @@ class Client: self.phone_code = phone_code self.first_name = first_name self.last_name = last_name + self.force_sms = force_sms self.workers = workers @@ -187,6 +193,9 @@ class Client: Raises: :class:`Error ` """ + if self.is_started: + raise ConnectionError("Client has already been started") + if self.BOT_TOKEN_RE.match(self.session_name): self.token = self.session_name self.session_name = self.session_name.split(":")[0] @@ -199,7 +208,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.api_key.api_id, + self.api_id, client=self ) @@ -235,6 +244,9 @@ class Client: """Use this method to manually stop the Client. Requires no parameters. """ + if not self.is_started: + raise ConnectionError("Client is already stopped") + self.is_started = False self.session.stop() @@ -252,8 +264,8 @@ class Client: r = self.send( functions.auth.ImportBotAuthorization( flags=0, - api_id=self.api_key.api_id, - api_hash=self.api_key.api_hash, + api_id=self.api_id, + api_hash=self.api_hash, bot_auth_token=self.token ) ) @@ -268,7 +280,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.api_key.api_id, + self.api_id, client=self ) @@ -301,8 +313,8 @@ class Client: r = self.send( functions.auth.SendCode( self.phone_number, - self.api_key.api_id, - self.api_key.api_hash + self.api_id, + self.api_hash ) ) except (PhoneMigrate, NetworkMigrate) as e: @@ -316,7 +328,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.api_key.api_id, + self.api_id, client=self ) self.session.start() @@ -324,8 +336,8 @@ class Client: r = self.send( functions.auth.SendCode( self.phone_number, - self.api_key.api_id, - self.api_key.api_hash + self.api_id, + self.api_hash ) ) break @@ -346,11 +358,19 @@ class Client: phone_registered = r.phone_registered phone_code_hash = r.phone_code_hash + if self.force_sms: + self.send( + functions.auth.ResendCode( + phone_number=self.phone_number, + phone_code_hash=phone_code_hash + ) + ) + while True: self.phone_code = ( input("Enter phone code: ") if self.phone_code is None else self.phone_code if type(self.phone_code) is str - else self.phone_code() + else str(self.phone_code()) ) try: @@ -634,7 +654,7 @@ class Client: if not isinstance(message, types.MessageEmpty): diff = self.send( functions.updates.GetChannelDifference( - channel=self.resolve_peer(update.message.to_id.channel_id), + channel=self.resolve_peer(int("-100" + str(update.message.to_id.channel_id))), filter=types.ChannelMessagesFilter( ranges=[types.MessageRange( min_id=update.message.id, @@ -787,32 +807,28 @@ class Client: Raises: :class:`Error ` """ - if self.is_started: - r = self.session.send(data) + if not self.is_started: + raise ConnectionError("Client has not been started") - self.fetch_peers(getattr(r, "users", [])) - self.fetch_peers(getattr(r, "chats", [])) + r = self.session.send(data) - return r - else: - raise ConnectionError("client '{}' is not started".format(self.session_name)) + self.fetch_peers(getattr(r, "users", [])) + self.fetch_peers(getattr(r, "chats", [])) + + return r def load_config(self): parser = ConfigParser() parser.read("config.ini") - if self.api_key is not None: - self.api_key = APIKey( - api_id=int(self.api_key[0]), - api_hash=self.api_key[1] - ) - elif parser.has_section("pyrogram"): - self.api_key = APIKey( - api_id=parser.getint("pyrogram", "api_id"), - api_hash=parser.get("pyrogram", "api_hash") - ) + if self.api_id and self.api_hash: + pass else: - raise AttributeError("No API Key found") + if parser.has_section("pyrogram"): + self.api_id = parser.getint("pyrogram", "api_id") + self.api_hash = parser.get("pyrogram", "api_hash") + else: + raise AttributeError("No API Key found") if self.proxy is not None: self.proxy = Proxy( @@ -899,6 +915,15 @@ class Client: offset_date = parse_dialogs(dialogs) log.info("Entities count: {}".format(len(self.peers_by_id))) + self.send( + functions.messages.GetDialogs( + 0, 0, types.InputPeerEmpty(), + self.DIALOGS_AT_ONCE, True + ) + ) + + log.info("Entities count: {}".format(len(self.peers_by_id))) + def resolve_peer(self, peer_id: int or str): """Use this method to get the *InputPeer* of a known *peer_id*. @@ -931,7 +956,7 @@ class Client: except (AttributeError, binascii.Error, struct.error): pass - peer_id = peer_id.lower().strip("@+") + peer_id = re.sub(r"[@+\s]", "", peer_id.lower()) try: int(peer_id) @@ -2161,7 +2186,7 @@ class Client: file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None - session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.api_key.api_id) + session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.api_id) session.start() try: @@ -2244,7 +2269,7 @@ class Client: self.test_mode, self.proxy, Auth(dc_id, self.test_mode, self.proxy).create(), - self.api_key.api_id + self.api_id ) session.start() @@ -2261,7 +2286,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.api_key.api_id + self.api_id ) session.start() @@ -2293,7 +2318,7 @@ class Client: ) if isinstance(r, types.upload.File): - with tempfile.NamedTemporaryFile('wb', delete=False) as f: + with tempfile.NamedTemporaryFile("wb", delete=False) as f: file_name = f.name while True: @@ -2325,14 +2350,14 @@ class Client: self.test_mode, self.proxy, Auth(r.dc_id, self.test_mode, self.proxy).create(), - self.api_key.api_id, + self.api_id, is_cdn=True ) cdn_session.start() try: - with tempfile.NamedTemporaryFile('wb', delete=False) as f: + with tempfile.NamedTemporaryFile("wb", delete=False) as f: file_name = f.name while True: diff --git a/pyrogram/crypto/aes.py b/pyrogram/crypto/aes.py index 8ca72535..05a01044 100644 --- a/pyrogram/crypto/aes.py +++ b/pyrogram/crypto/aes.py @@ -16,33 +16,52 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +import logging + +log = logging.getLogger(__name__) + try: import tgcrypto -except ImportError as e: - e.msg = ( - "TgCrypto is missing and Pyrogram can't run without. " - "Please install it using \"pip3 install tgcrypto\". " +except ImportError: + log.warning( + "TgCrypto is missing! " + "Pyrogram will work the same, but at a much slower speed. " "More info: https://docs.pyrogram.ml/resources/TgCrypto" ) - - raise e + is_fast = False + import pyaes +else: + log.info("Using TgCrypto") + is_fast = True +# TODO: Ugly IFs class AES: @classmethod def ige_encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: - return tgcrypto.ige_encrypt(data, key, iv) + if is_fast: + return tgcrypto.ige_encrypt(data, key, iv) + else: + return cls.ige(data, key, iv, True) @classmethod def ige_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: - return tgcrypto.ige_decrypt(data, key, iv) + if is_fast: + return tgcrypto.ige_decrypt(data, key, iv) + else: + return cls.ige(data, key, iv, False) @staticmethod def ctr_decrypt(data: bytes, key: bytes, iv: bytes, offset: int) -> bytes: replace = int.to_bytes(offset // 16, 4, "big") iv = iv[:-4] + replace - return tgcrypto.ctr_decrypt(data, key, iv) + if is_fast: + return tgcrypto.ctr_decrypt(data, key, iv) + else: + ctr = pyaes.AESModeOfOperationCTR(key) + ctr._counter._counter = list(iv) + return ctr.decrypt(data) @staticmethod def xor(a: bytes, b: bytes) -> bytes: @@ -51,3 +70,23 @@ class AES: len(a), "big", ) + + @classmethod + def ige(cls, data: bytes, key: bytes, iv: bytes, encrypt: bool) -> bytes: + cipher = pyaes.AES(key) + + iv_1 = iv[:16] + iv_2 = iv[16:] + + data = [data[i: i + 16] for i in range(0, len(data), 16)] + + if encrypt: + for i, chunk in enumerate(data): + iv_1 = data[i] = cls.xor(cipher.encrypt(cls.xor(chunk, iv_1)), iv_2) + iv_2 = chunk + else: + for i, chunk in enumerate(data): + iv_2 = data[i] = cls.xor(cipher.decrypt(cls.xor(chunk, iv_2)), iv_1) + iv_1 = chunk + + return b"".join(data) diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index 46d722fc..5d54ff1f 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -87,7 +87,7 @@ class Session: test_mode: bool, proxy: type, auth_key: bytes, - api_id: str, + api_id: int, is_cdn: bool = False, client: pyrogram = None): if not Session.notice_displayed: diff --git a/requirements.txt b/requirements.txt index 21c697f1..3216c15d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -pysocks -tgcrypto \ No newline at end of file +pyaes>=1.6.1 +pysocks>=1.6.8 \ No newline at end of file diff --git a/requirements_extras.txt b/requirements_extras.txt new file mode 100644 index 00000000..1d101a7e --- /dev/null +++ b/requirements_extras.txt @@ -0,0 +1 @@ +tgcrypto>=1.0.4 \ No newline at end of file diff --git a/setup.py b/setup.py index c82c1a9d..7a8e0132 100644 --- a/setup.py +++ b/setup.py @@ -25,8 +25,8 @@ from compiler.api import compiler as api_compiler from compiler.error import compiler as error_compiler -def requirements(): - with open("requirements.txt", encoding="utf-8") as r: +def read(file: str) -> list: + with open(file, encoding="utf-8") as r: return [i.strip() for i in r] @@ -82,5 +82,6 @@ setup( python_requires="~=3.4", packages=find_packages(exclude=["compiler*"]), zip_safe=False, - install_requires=requirements() + install_requires=read("requirements.txt"), + extras_require={"tgcrypto": read("requirements_extras.txt")} )