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/client/client.py b/pyrogram/client/client.py index 9afd7b53..c14ab380 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -56,12 +56,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 @@ -83,10 +77,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, @@ -112,13 +109,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. @@ -132,17 +134,20 @@ class Client: def __init__(self, session_name: str, - api_key: tuple or APIKey = None, + api_id: int = 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 = api_id + self.api_hash = api_hash self.proxy = proxy self.test_mode = test_mode @@ -151,6 +156,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 @@ -189,6 +195,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] @@ -201,7 +210,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.api_key.api_id, + self.api_id, client=self ) @@ -237,6 +246,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() @@ -254,8 +266,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 ) ) @@ -270,7 +282,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.api_key.api_id, + self.api_id, client=self ) @@ -303,8 +315,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: @@ -318,7 +330,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.api_key.api_id, + self.api_id, client=self ) self.session.start() @@ -326,8 +338,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 @@ -348,6 +360,14 @@ 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 @@ -636,7 +656,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, @@ -813,32 +833,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( @@ -925,6 +941,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*. @@ -957,7 +982,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) @@ -2187,7 +2212,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: @@ -2270,7 +2295,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() @@ -2287,7 +2312,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.api_key.api_id + self.api_id ) session.start() @@ -2319,7 +2344,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: @@ -2351,14 +2376,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 a5b0c3c8..05a01044 100644 --- a/pyrogram/crypto/aes.py +++ b/pyrogram/crypto/aes.py @@ -1 +1,92 @@ -# Pyrogram - Telegram MTProto API Client Library for Python # Copyright (C) 2017-2018 Dan Tès # # This file is part of Pyrogram. # # Pyrogram is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pyrogram is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . 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\". " "More info: https://docs.pyrogram.ml/resources/TgCrypto" ) raise e class AES: @classmethod def ige_encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: return tgcrypto.ige_encrypt(data, key, iv) @classmethod def ige_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: return tgcrypto.ige_decrypt(data, key, iv) @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) @staticmethod def xor(a: bytes, b: bytes) -> bytes: return int.to_bytes( int.from_bytes(a, "big") ^ int.from_bytes(b, "big"), len(a), "big", ) \ No newline at end of file +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# 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: + log.warning( + "TgCrypto is missing! " + "Pyrogram will work the same, but at a much slower speed. " + "More info: https://docs.pyrogram.ml/resources/TgCrypto" + ) + 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: + 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: + 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 + + 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: + return int.to_bytes( + int.from_bytes(a, "big") ^ int.from_bytes(b, "big"), + 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")} )