diff --git a/README.rst b/README.rst index e14ec7cc..554ef7a1 100644 --- a/README.rst +++ b/README.rst @@ -80,6 +80,12 @@ Installation .. code:: shell $ pip install --upgrade pyrogram + +- Or, with TgCrypto_: + + .. code:: shell + + $ pip install --upgrade pyrogram[tgcrypto] Configuration ------------- @@ -168,7 +174,7 @@ License .. _`Email`: admin@pyrogram.ml -.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto +.. _TgCrypto: https://github.com/pyrogram/tgcrypto .. _`GNU Lesser General Public License v3 or later (LGPLv3+)`: COPYING.lesser @@ -195,8 +201,8 @@ License • Community -

+ +

Scheme Layer 75 diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 934197e8..db7a3a84 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -23,11 +23,12 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès 10 * 1024 * 1024 else False - is_big = False # Treat all files as not-big to have the server check for the md5 sum + is_big = True if file_size > 10 * 1024 * 1024 else False is_missing_part = True if file_id is not None else False 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.config.api_id) + session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.api_key.api_id) session.start() try: @@ -1723,11 +1831,7 @@ class Client: volume_id: int = None, local_id: int = None, secret: int = None, - version: int = 0): - # TODO: Refine - # TODO: Use proper file name and extension - # TODO: Remove redundant code - + version: int = 0) -> str: if dc_id != self.dc_id: exported_auth = self.send( functions.auth.ExportAuthorization( @@ -1740,7 +1844,7 @@ class Client: self.test_mode, self.proxy, Auth(dc_id, self.test_mode, self.proxy).create(), - self.config.api_id + self.api_key.api_id ) session.start() @@ -1757,7 +1861,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.config.api_id + self.api_key.api_id ) session.start() @@ -1775,7 +1879,8 @@ class Client: version=version ) - limit = 512 * 1024 + file_name = str(MsgId()) + limit = 1024 * 1024 offset = 0 try: @@ -1788,7 +1893,7 @@ class Client: ) if isinstance(r, types.upload.File): - with open("_".join([str(id), str(access_hash), str(version)]) + ".jpg", "wb") as f: + with open(file_name, "wb") as f: while True: chunk = r.bytes @@ -1796,6 +1901,9 @@ class Client: break f.write(chunk) + f.flush() + os.fsync(f.fileno()) + offset += limit r = session.send( @@ -1805,20 +1913,21 @@ class Client: limit=limit ) ) + if isinstance(r, types.upload.FileCdnRedirect): cdn_session = Session( r.dc_id, self.test_mode, self.proxy, Auth(r.dc_id, self.test_mode, self.proxy).create(), - self.config.api_id, + self.api_key.api_id, is_cdn=True ) cdn_session.start() try: - with open("_".join([str(id), str(access_hash), str(version)]) + ".jpg", "wb") as f: + with open(file_name, "wb") as f: while True: r2 = cdn_session.send( functions.upload.GetCdnFile( @@ -1830,27 +1939,48 @@ class Client: ) if isinstance(r2, types.upload.CdnFileReuploadNeeded): - session.send( - functions.upload.ReuploadCdnFile( - file_token=r.file_token, - request_token=r2.request_token + try: + session.send( + functions.upload.ReuploadCdnFile( + file_token=r.file_token, + request_token=r2.request_token + ) ) - ) - continue - elif isinstance(r2, types.upload.CdnFile): - chunk = r2.bytes - - if not chunk: + except VolumeLocNotFound: break + else: + continue - # https://core.telegram.org/cdn#decrypting-files - decrypted_chunk = AES.ctr_decrypt(chunk, r.encryption_key, r.encryption_iv, offset) + chunk = r2.bytes - # TODO: https://core.telegram.org/cdn#verifying-files - # TODO: Save to temp file, flush each chunk, rename to full if everything is ok + # https://core.telegram.org/cdn#decrypting-files + decrypted_chunk = AES.ctr_decrypt( + chunk, + r.encryption_key, + r.encryption_iv, + offset + ) - f.write(decrypted_chunk) - offset += limit + hashes = session.send( + functions.upload.GetCdnFileHashes( + r.file_token, + offset + ) + ) + + # https://core.telegram.org/cdn#verifying-files + for i, h in enumerate(hashes): + cdn_chunk = decrypted_chunk[h.limit * i: h.limit * (i + 1)] + assert h.hash == sha256(cdn_chunk).digest(), "Invalid CDN hash part {}".format(i) + + f.write(decrypted_chunk) + f.flush() + os.fsync(f.fileno()) + + if len(chunk) < limit: + break + + offset += limit except Exception as e: log.error(e) finally: @@ -1858,7 +1988,7 @@ class Client: except Exception as e: log.error(e) else: - return True + return file_name finally: session.stop() @@ -2210,3 +2340,78 @@ class Client: reply_to_msg_id=reply_to_message_id ) ) + + def download_media(self, message: types.Message, file_name: str = None): + done = Event() + media = message.media if isinstance(message, types.Message) else message + + self.download_queue.put((media, file_name, done)) + + done.wait() + + def add_contacts(self, contacts: list): + """Use this method to add contacts to your Telegram address book. + + Args: + contacts (:obj:`list`): + A list of :obj:`InputPhoneContact ` + + Returns: + On success, the added contacts are returned. + + Raises: + :class:`pyrogram.Error` + """ + imported_contacts = self.send( + functions.contacts.ImportContacts( + contacts=contacts + ) + ) + + self.fetch_peers(imported_contacts.users) + + return imported_contacts + + def delete_contacts(self, ids: list): + """Use this method to delete contacts from your Telegram address book + + Args: + ids (:obj:`list`): + A list of unique identifiers for the target users. Can be an ID (int), a username (string) + or phone number (string). + + Returns: + True on success. + + Raises: + :class:`pyrogram.Error` + """ + contacts = [] + + for i in ids: + try: + input_user = self.resolve_peer(i) + except PeerIdInvalid: + continue + else: + if isinstance(input_user, types.InputPeerUser): + contacts.append(input_user) + + return self.send( + functions.contacts.DeleteContacts( + id=contacts + ) + ) + + def get_contacts(self, _hash: int = 0): + while True: + try: + contacts = self.send(functions.contacts.GetContacts(_hash)) + except FloodWait as e: + log.info("Get contacts flood wait: {}".format(e.x)) + time.sleep(e.x) + continue + else: + log.info("Contacts count: {}".format(len(contacts.users))) + self.fetch_peers(contacts.users) + return contacts diff --git a/pyrogram/client/emoji.py b/pyrogram/client/emoji.py index 6b85f90f..853f810d 100644 --- a/pyrogram/client/emoji.py +++ b/pyrogram/client/emoji.py @@ -1,3 +1,22 @@ +# 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 . + + class Emoji: HELMET_WITH_WHITE_CROSS_TYPE_1_2 = "\u26d1\U0001f3fb" HELMET_WITH_WHITE_CROSS_TYPE_3 = "\u26d1\U0001f3fc" diff --git a/pyrogram/client/input_phone_contact.py b/pyrogram/client/input_phone_contact.py new file mode 100644 index 00000000..002a3121 --- /dev/null +++ b/pyrogram/client/input_phone_contact.py @@ -0,0 +1,43 @@ +# 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 . + +from pyrogram.api.types import InputPhoneContact as RawInputPhoneContact + + +class InputPhoneContact: + """This object represents a Phone Contact to be added in your Telegram address book. + It is intended to be used with :obj:`pyrogram.Client.add_contacts` + + Args: + phone (:obj:`str`): + Contact's phone number + + first_name (:obj:`str`): + Contact's first name + + last_name (:obj:`str`, optional): + Contact's last name + """ + + def __new__(cls, phone: str, first_name: str, last_name: str = ""): + return RawInputPhoneContact( + client_id=0, + phone="+" + phone.strip("+"), + first_name=first_name, + last_name=last_name + ) diff --git a/pyrogram/connection/transport/tcp/tcp.py b/pyrogram/connection/transport/tcp/tcp.py index a762a39e..a6195586 100644 --- a/pyrogram/connection/transport/tcp/tcp.py +++ b/pyrogram/connection/transport/tcp/tcp.py @@ -44,6 +44,11 @@ class TCP(socks.socksocket): password=proxy.password ) + log.info("Using proxy {}:{}".format( + proxy.hostname, + proxy.port + )) + def close(self): try: self.shutdown(socket.SHUT_RDWR) diff --git a/pyrogram/crypto/aes.py b/pyrogram/crypto/aes.py index dfc20b66..c571fb2d 100644 --- a/pyrogram/crypto/aes.py +++ b/pyrogram/crypto/aes.py @@ -23,7 +23,7 @@ log = logging.getLogger(__name__) try: import tgcrypto except ImportError: - logging.warning( + log.warning( "TgCrypto is missing! " "Pyrogram will work the same, but at a much slower speed. " "More info: https://docs.pyrogram.ml/resources/TgCrypto" diff --git a/pyrogram/session/auth.py b/pyrogram/session/auth.py index 741e9a44..f48242e6 100644 --- a/pyrogram/session/auth.py +++ b/pyrogram/session/auth.py @@ -51,12 +51,12 @@ class Auth: self.test_mode = test_mode self.connection = Connection(DataCenter(dc_id, test_mode), proxy) - self.msg_id = MsgId() - def pack(self, data: Object) -> bytes: + @staticmethod + def pack(data: Object) -> bytes: return ( bytes(8) - + Long(self.msg_id()) + + Long(MsgId()) + Int(len(data.write())) + data.write() ) diff --git a/pyrogram/session/internals/msg_factory.py b/pyrogram/session/internals/msg_factory.py index adf29773..f599cd6f 100644 --- a/pyrogram/session/internals/msg_factory.py +++ b/pyrogram/session/internals/msg_factory.py @@ -26,14 +26,13 @@ not_content_related = [Ping, HttpWait, MsgsAck, MsgContainer] class MsgFactory: - def __init__(self, msg_id: MsgId): - self.msg_id = msg_id + def __init__(self): self.seq_no = SeqNo() def __call__(self, body: Object) -> Message: return Message( body, - self.msg_id(), + MsgId(), self.seq_no(type(body) not in not_content_related), len(body) ) diff --git a/pyrogram/session/internals/msg_id.py b/pyrogram/session/internals/msg_id.py index cf8c0402..99aa9d14 100644 --- a/pyrogram/session/internals/msg_id.py +++ b/pyrogram/session/internals/msg_id.py @@ -21,17 +21,15 @@ from time import time class MsgId: - def __init__(self, delta_time: float = 0.0): - self.delta_time = delta_time - self.last_time = 0 - self.offset = 0 - self.lock = Lock() + last_time = 0 + offset = 0 + lock = Lock() - def __call__(self) -> int: - with self.lock: + def __new__(cls) -> int: + with cls.lock: now = time() - self.offset = self.offset + 4 if now == self.last_time else 0 - msg_id = int((now + self.delta_time) * 2 ** 32) + self.offset - self.last_time = now + cls.offset = cls.offset + 4 if now == cls.last_time else 0 + msg_id = int(now * 2 ** 32) + cls.offset + cls.last_time = now return msg_id diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index 9010e2fe..7dd0cba4 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -89,9 +89,8 @@ class Session: self.auth_key = auth_key self.auth_key_id = sha1(auth_key).digest()[-8:] - self.msg_id = MsgId() - self.session_id = Long(self.msg_id()) - self.msg_factory = MsgFactory(self.msg_id) + self.session_id = Long(MsgId()) + self.msg_factory = MsgFactory() self.current_salt = None @@ -146,7 +145,7 @@ class Session: self.ping_thread.start() log.info("Connection inited: Layer {}".format(layer)) - except (OSError, TimeoutError): + except (OSError, TimeoutError, Error): self.stop() else: break @@ -338,7 +337,10 @@ class Session: while True: packet = self.connection.recv() - if packet is None or (len(packet) == 4 and Int.read(BytesIO(packet)) == -404): + if packet is None or len(packet) == 4: + if packet: + log.warning("Server sent \"{}\"".format(Int.read(BytesIO(packet)))) + if self.is_connected.is_set(): Thread(target=self.restart, name="RestartThread").start() break diff --git a/setup.py b/setup.py index 4304163d..8eded9ca 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,14 @@ setup( ], packages=find_packages(), zip_safe=False, - install_requires=["pyaes", "pysocks"], + install_requires=[ + "pyaes", + "pysocks" + ], + extras_require={ + "tgcrypto": [ + "tgcrypto" + ] + }, include_package_data=True, )