From 5826470c77f25f71f83f2aadc43f69e0f2577429 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 17 Feb 2018 16:06:22 +0100 Subject: [PATCH 01/27] Add TgCrypto as an extra requirement --- setup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8096bbd7..10294494 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,14 @@ setup( ], packages=find_packages(), zip_safe=False, - install_requires=["pyaes", "pysocks"], + install_requires=[ + "pyaes", + "pysocks" + ], + extras_require={ + "tgcrypto": [ + "tgcrypto" + ] + }, include_package_data=True, ) From 74a7adcbdd9afc1a499897413c31845bcb2fcc65 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 17 Feb 2018 16:06:36 +0100 Subject: [PATCH 02/27] Don't use logging directly --- pyrogram/crypto/aes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From aaf1b5c862a1351271f2f72aa823e3272f8b61ad Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 17 Feb 2018 16:29:03 +0100 Subject: [PATCH 03/27] Update to v0.6.1 --- pyrogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 934197e8..6dfc2b39 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès Date: Sat, 17 Feb 2018 16:44:54 +0100 Subject: [PATCH 04/27] Update README.rst --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index e14ec7cc..c483b209 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 ------------- From 877e0985cad7356cd5c6027ce2767a14d121ecad Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 17 Feb 2018 21:52:27 +0100 Subject: [PATCH 05/27] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c483b209..292a0e4f 100644 --- a/README.rst +++ b/README.rst @@ -174,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 From 3bd3d99e6ad736941f6bd7206b40ff9bfc73ad40 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 18 Feb 2018 15:03:33 +0100 Subject: [PATCH 06/27] Rewrite get_file --- pyrogram/client/client.py | 96 +++++++++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index b3272901..54ddf256 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1723,11 +1723,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( @@ -1763,19 +1759,23 @@ class Client: session.start() if volume_id: # Photos are accessed by volume_id, local_id, secret + file_name = "_".join(str(i) for i in [dc_id, volume_id, local_id, secret]) + location = types.InputFileLocation( volume_id=volume_id, local_id=local_id, secret=secret ) else: # Any other file can be more easily accessed by id and access_hash + file_name = "_".join(str(i) for i in [dc_id, id, access_hash, version]) + location = types.InputDocumentFileLocation( id=id, access_hash=access_hash, version=version ) - limit = 512 * 1024 + limit = 1024 * 1024 offset = 0 try: @@ -1788,7 +1788,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 +1796,9 @@ class Client: break f.write(chunk) + f.flush() + os.fsync(f.fileno()) + offset += limit r = session.send( @@ -1805,6 +1808,7 @@ class Client: limit=limit ) ) + if isinstance(r, types.upload.FileCdnRedirect): cdn_session = Session( r.dc_id, @@ -1818,39 +1822,63 @@ class Client: cdn_session.start() try: - with open("_".join([str(id), str(access_hash), str(version)]) + ".jpg", "wb") as f: - while True: - r2 = cdn_session.send( - functions.upload.GetCdnFile( - location=location, - file_token=r.file_token, - offset=offset, - limit=limit - ) + r2 = cdn_session.send( + functions.upload.GetCdnFile( + location=location, + file_token=r.file_token, + offset=offset, + limit=limit + ) + ) + + if isinstance(r2, types.upload.CdnFileReuploadNeeded): + session.send( + functions.upload.ReuploadCdnFile( + file_token=r.file_token, + request_token=r2.request_token ) - - if isinstance(r2, types.upload.CdnFileReuploadNeeded): - 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: + ) + else: + with open(file_name, "wb") as f: + while True: + if not isinstance(r2, types.upload.CdnFile): break - # 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 + ) + + hashes = session.send( + functions.upload.GetCdnFileHashes( + r.file_token, + offset + ) + ) + + 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()) + offset += limit + + r2 = cdn_session.send( + functions.upload.GetCdnFile( + location=location, + file_token=r.file_token, + offset=offset, + limit=limit + ) + ) except Exception as e: log.error(e) finally: @@ -1858,7 +1886,7 @@ class Client: except Exception as e: log.error(e) else: - return True + return file_name finally: session.stop() From d89d238d303ccba82dbf4b40c7a6f4c16b22e7d7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 18 Feb 2018 17:31:00 +0100 Subject: [PATCH 07/27] Fix BadMsgNotification error_code 32 --- pyrogram/session/auth.py | 6 +++--- pyrogram/session/internals/msg_factory.py | 5 ++--- pyrogram/session/internals/msg_id.py | 18 ++++++++---------- pyrogram/session/session.py | 5 ++--- 4 files changed, 15 insertions(+), 19 deletions(-) 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..b86fc992 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 From 15561d19d577206461bdffb871e3ee3a9914ab10 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 18 Feb 2018 18:11:33 +0100 Subject: [PATCH 08/27] Add initial support for downloading media --- pyrogram/client/client.py | 181 ++++++++++++++++++++++++++------------ 1 file changed, 124 insertions(+), 57 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 54ddf256..993b9bf9 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -27,6 +27,7 @@ import threading import time from collections import namedtuple from configparser import ConfigParser +from datetime import datetime from hashlib import sha256, md5 from queue import Queue from signal import signal, SIGINT, SIGTERM, SIGABRT @@ -39,8 +40,8 @@ from pyrogram.api.errors import ( PhoneNumberUnoccupied, PhoneCodeInvalid, PhoneCodeHashEmpty, PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded, PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing, - ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned -) + ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned, + VolumeLocNotFound) from pyrogram.api.types import ( User, Chat, Channel, PeerUser, PeerChannel, @@ -49,6 +50,7 @@ from pyrogram.api.types import ( ) from pyrogram.crypto import AES from pyrogram.session import Auth, Session +from pyrogram.session.internals import MsgId from .input_media import InputMedia from .style import Markdown, HTML @@ -103,6 +105,7 @@ class Client: INVITE_LINK_RE = re.compile(r"^(?:https?://)?t\.me/joinchat/(.+)$") DIALOGS_AT_ONCE = 100 UPDATES_WORKERS = 2 + DOWNLOAD_WORKERS = 1 def __init__(self, session_name: str, @@ -148,6 +151,8 @@ class Client: self.update_queue = Queue() self.update_handler = None + self.download_queue = Queue() + def start(self): """Use this method to start the Client after creating it. Requires no parameters. @@ -176,7 +181,7 @@ class Client: self.password = None self.save_session() - self.rnd_id = self.session.msg_id + self.rnd_id = MsgId self.get_dialogs() for i in range(self.UPDATES_WORKERS): @@ -185,6 +190,9 @@ class Client: for i in range(self.workers): Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start() + for i in range(self.DOWNLOAD_WORKERS): + Thread(target=self.download_worker, name="DownloadWorker#{}".format(i + 1)).start() + mimetypes.init() def stop(self): @@ -199,6 +207,9 @@ class Client: for _ in range(self.workers): self.update_queue.put(None) + for _ in range(self.DOWNLOAD_WORKERS): + self.download_queue.put(None) + def fetch_peers(self, entities: list): for entity in entities: if isinstance(entity, User): @@ -260,6 +271,67 @@ class Client: if username is not None: self.peers_by_username[username] = input_peer + def download_worker(self): + name = threading.current_thread().name + log.debug("{} started".format(name)) + + while True: + message = self.download_queue.get() + + if message is None: + break + + message, done = message + + try: + if isinstance(message.media, types.MessageMediaDocument): + document = message.media.document + + if isinstance(document, types.Document): + file_name = "doc_{}{}".format( + datetime.fromtimestamp(document.date).strftime("%Y-%m-%d_%H-%M-%S"), + mimetypes.guess_extension(document.mime_type) or ".unknown" + ) + + for i in document.attributes: + if isinstance(i, types.DocumentAttributeFilename): + file_name = i.file_name + break + elif isinstance(i, types.DocumentAttributeSticker): + file_name = file_name.replace("doc", "sticker") + elif isinstance(i, types.DocumentAttributeAudio): + file_name = file_name.replace("doc", "audio") + elif isinstance(i, types.DocumentAttributeVideo): + file_name = file_name.replace("doc", "video") + elif isinstance(i, types.DocumentAttributeAnimated): + file_name = file_name.replace("doc", "gif") + + tmp_file_name = self.get_file( + dc_id=document.dc_id, + id=document.id, + access_hash=document.access_hash, + version=document.version + ) + + i = 1 + while True: + try: + os.renames("./{}".format(tmp_file_name), "./downloads/{}".format( + ".".join(file_name.split(".")[:-1]) + + (" ({}).".format(i) if i > 1 else ".") + + file_name.split(".")[-1] + )) + except FileExistsError: + i += 1 + else: + break + + done.set() + except Exception as e: + log.error(e, exc_info=True) + + log.debug("{} stopped".format(name)) + def updates_worker(self): name = threading.current_thread().name log.debug("{} started".format(name)) @@ -1667,8 +1739,7 @@ class Client: part_size = 512 * 1024 file_size = os.path.getsize(path) file_total_parts = math.ceil(file_size / part_size) - # is_big = True if file_size > 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 @@ -1759,22 +1830,19 @@ class Client: session.start() if volume_id: # Photos are accessed by volume_id, local_id, secret - file_name = "_".join(str(i) for i in [dc_id, volume_id, local_id, secret]) - location = types.InputFileLocation( volume_id=volume_id, local_id=local_id, secret=secret ) else: # Any other file can be more easily accessed by id and access_hash - file_name = "_".join(str(i) for i in [dc_id, id, access_hash, version]) - location = types.InputDocumentFileLocation( id=id, access_hash=access_hash, version=version ) + file_name = str(MsgId()) limit = 1024 * 1024 offset = 0 @@ -1822,63 +1890,57 @@ class Client: cdn_session.start() try: - r2 = cdn_session.send( - functions.upload.GetCdnFile( - location=location, - file_token=r.file_token, - offset=offset, - limit=limit - ) - ) - - if isinstance(r2, types.upload.CdnFileReuploadNeeded): - session.send( - functions.upload.ReuploadCdnFile( - file_token=r.file_token, - request_token=r2.request_token + with open(file_name, "wb") as f: + while True: + r2 = cdn_session.send( + functions.upload.GetCdnFile( + location=location, + file_token=r.file_token, + offset=offset, + limit=limit + ) ) - ) - else: - with open(file_name, "wb") as f: - while True: - if not isinstance(r2, types.upload.CdnFile): + + if isinstance(r2, types.upload.CdnFileReuploadNeeded): + try: + session.send( + functions.upload.ReuploadCdnFile( + file_token=r.file_token, + request_token=r2.request_token + ) + ) + except VolumeLocNotFound: break + else: + continue - chunk = r2.bytes + chunk = r2.bytes - # https://core.telegram.org/cdn#decrypting-files - decrypted_chunk = AES.ctr_decrypt( - chunk, - r.encryption_key, - r.encryption_iv, + # https://core.telegram.org/cdn#decrypting-files + decrypted_chunk = AES.ctr_decrypt( + chunk, + r.encryption_key, + r.encryption_iv, + offset + ) + + hashes = session.send( + functions.upload.GetCdnFileHashes( + r.file_token, offset ) + ) - 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) - 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()) - f.write(decrypted_chunk) - f.flush() - os.fsync(f.fileno()) - - offset += limit - - r2 = cdn_session.send( - functions.upload.GetCdnFile( - location=location, - file_token=r.file_token, - offset=offset, - limit=limit - ) - ) + offset += limit except Exception as e: log.error(e) finally: @@ -2238,3 +2300,8 @@ class Client: reply_to_msg_id=reply_to_message_id ) ) + + def download_media(self, message: types.Message): + done = Event() + self.download_queue.put((message, done)) + done.wait() From 02da3fe94750cd3ca4afc313482d0676b5bc2c2e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 18 Feb 2018 20:33:33 +0100 Subject: [PATCH 09/27] Handle TCP server errors --- pyrogram/session/session.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index b86fc992..8978b4de 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -337,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 From 46e310722aebc3a2c74b4caa90bfda300666f5b7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 19 Feb 2018 13:11:35 +0100 Subject: [PATCH 10/27] Improve download_media --- pyrogram/client/client.py | 71 ++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 993b9bf9..6dabd1f4 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -276,35 +276,36 @@ class Client: log.debug("{} started".format(name)) while True: - message = self.download_queue.get() + media = self.download_queue.get() - if message is None: + if media is None: break - message, done = message + media, file_name, done = media try: - if isinstance(message.media, types.MessageMediaDocument): - document = message.media.document + if isinstance(media, types.MessageMediaDocument): + document = media.document if isinstance(document, types.Document): - file_name = "doc_{}{}".format( - datetime.fromtimestamp(document.date).strftime("%Y-%m-%d_%H-%M-%S"), - mimetypes.guess_extension(document.mime_type) or ".unknown" - ) + if not file_name: + file_name = "doc_{}{}".format( + datetime.fromtimestamp(document.date).strftime("%Y-%m-%d_%H-%M-%S"), + mimetypes.guess_extension(document.mime_type) or ".unknown" + ) - for i in document.attributes: - if isinstance(i, types.DocumentAttributeFilename): - file_name = i.file_name - break - elif isinstance(i, types.DocumentAttributeSticker): - file_name = file_name.replace("doc", "sticker") - elif isinstance(i, types.DocumentAttributeAudio): - file_name = file_name.replace("doc", "audio") - elif isinstance(i, types.DocumentAttributeVideo): - file_name = file_name.replace("doc", "video") - elif isinstance(i, types.DocumentAttributeAnimated): - file_name = file_name.replace("doc", "gif") + for i in document.attributes: + if isinstance(i, types.DocumentAttributeFilename): + file_name = i.file_name + break + elif isinstance(i, types.DocumentAttributeSticker): + file_name = file_name.replace("doc", "sticker") + elif isinstance(i, types.DocumentAttributeAudio): + file_name = file_name.replace("doc", "audio") + elif isinstance(i, types.DocumentAttributeVideo): + file_name = file_name.replace("doc", "video") + elif isinstance(i, types.DocumentAttributeAnimated): + file_name = file_name.replace("doc", "gif") tmp_file_name = self.get_file( dc_id=document.dc_id, @@ -313,18 +314,12 @@ class Client: version=document.version ) - i = 1 - while True: - try: - os.renames("./{}".format(tmp_file_name), "./downloads/{}".format( - ".".join(file_name.split(".")[:-1]) - + (" ({}).".format(i) if i > 1 else ".") - + file_name.split(".")[-1] - )) - except FileExistsError: - i += 1 - else: - break + try: + os.remove("./downloads/{}".format(file_name)) + except FileNotFoundError: + pass + + os.renames("./{}".format(tmp_file_name), "./downloads/{}".format(file_name)) done.set() except Exception as e: @@ -1940,6 +1935,9 @@ class Client: f.flush() os.fsync(f.fileno()) + if len(chunk) < limit: + break + offset += limit except Exception as e: log.error(e) @@ -2301,7 +2299,10 @@ class Client: ) ) - def download_media(self, message: types.Message): + def download_media(self, message: types.Message, file_name: str = None): done = Event() - self.download_queue.put((message, done)) + media = message.media if isinstance(message, types.Message) else message + + self.download_queue.put((media, file_name, done)) + done.wait() From 282e2bb79b60bf853bee7b0d308bba15d684f545 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 19 Feb 2018 13:25:44 +0100 Subject: [PATCH 11/27] Add missing notice --- pyrogram/client/emoji.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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" From 027f843047cc9766c9498d4c66f4d17efe801cc6 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 19 Feb 2018 13:43:16 +0100 Subject: [PATCH 12/27] Fix deadlock in case the message doesn't contain any media --- pyrogram/client/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 6dabd1f4..4329672a 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -320,10 +320,10 @@ class Client: pass os.renames("./{}".format(tmp_file_name), "./downloads/{}".format(file_name)) - - done.set() except Exception as e: log.error(e, exc_info=True) + finally: + done.set() log.debug("{} stopped".format(name)) From 8b7519b53610c806514f60930e48571728483674 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 19 Feb 2018 15:12:19 +0100 Subject: [PATCH 13/27] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 292a0e4f..554ef7a1 100644 --- a/README.rst +++ b/README.rst @@ -201,8 +201,8 @@ License • Community -

+ +

Scheme Layer 75 From 7d727381532223dfe42c48c5ac08f1e6bd6b48a8 Mon Sep 17 00:00:00 2001 From: RussFP <1pyxa1@gmail.com> Date: Tue, 20 Feb 2018 14:20:34 +0300 Subject: [PATCH 14/27] add add_contacts, delete_contacts, get_contacts --- pyrogram/client/client.py | 102 +++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 4329672a..98cff8bb 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -45,14 +45,17 @@ from pyrogram.api.errors import ( from pyrogram.api.types import ( User, Chat, Channel, PeerUser, PeerChannel, + InputUser, InputPeerEmpty, InputPeerSelf, - InputPeerUser, InputPeerChat, InputPeerChannel + InputPeerUser, InputPeerChat, InputPeerChannel, + InputPhoneContact ) from pyrogram.crypto import AES from pyrogram.session import Auth, Session from pyrogram.session.internals import MsgId from .input_media import InputMedia from .style import Markdown, HTML +from typing import List, Union log = logging.getLogger(__name__) @@ -2306,3 +2309,100 @@ class Client: self.download_queue.put((media, file_name, done)) done.wait() + + def add_contacts(self, + phone: Union[int, str] = None, + first_name: str = None, + last_name: str = None, + input_phone_contact_list: List[InputPhoneContact] = None): + if (phone is None or first_name is None) and \ + input_phone_contact_list is None: + log.warning("(phone and first_name) or input_phone_contact_list " + "must be not None") + return None + + if phone is not None and first_name is not None: + if str(phone)[0] != '+': + phone = '+' + str(phone) + input_phone_contact_list = [] + input_phone_contact = InputPhoneContact(client_id=0, + phone=phone, + first_name=first_name, + last_name=last_name or '') + input_phone_contact_list.append(input_phone_contact) + + # make sure that we send only InputPhoneContact + inner_input_phone_contact_list = [] + for contact in input_phone_contact_list: + if isinstance(contact, InputPhoneContact): + inner_input_phone_contact_list.append(contact) + + imported_contacts = self.send( + functions.contacts.ImportContacts(inner_input_phone_contact_list)) + + for user in imported_contacts.users: + if isinstance(user, User): + if user.id in self.peers_by_id: + continue + + if user.access_hash is None: + continue + + input_peer = InputPeerUser( + user_id=user.id, + access_hash=user.access_hash + ) + + self.peers_by_id[user.id] = input_peer + + if user.username is not None: + self.peers_by_username[user.username] = input_peer + + return imported_contacts + + def delete_contacts(self, _id: int = None, + ids_list: Union[ + List[int], List[InputUser]] = None): + if _id is None and ids_list is None: + log.warning('id or ids_list must be not None') + return False + + contacts = self.get_contacts() + + if _id is not None: + if not isinstance(_id, int): + log.warning('id is not int') + return False + + input_user = None + for user in contacts.users: + if isinstance(user, User): + if _id == user.id: + input_user = InputUser(user_id=user.id, + access_hash=user.access_hash) + break + + ids_list = [input_user] + + inner_ids_list = [] + for _id in ids_list: + if isinstance(_id, InputUser): + inner_ids_list.append(_id) + + if isinstance(_id, int): + input_user = None + for user in contacts.users: + if isinstance(user, User): + if _id == user.id: + input_user = InputUser( + user_id=user.id, + access_hash=user.access_hash) + break + inner_ids_list.append(input_user) + + res = self.send(functions.contacts.DeleteContacts(inner_ids_list)) + return res + + def get_contacts(self, _hash: int = 0): + return self.send(functions.contacts.GetContacts(_hash)) + From 2ab8fbe04776e50b05e66f4d1d3ff7f96e81e4eb Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 20 Feb 2018 15:01:28 +0100 Subject: [PATCH 15/27] Add InputPhoneContact type --- pyrogram/__init__.py | 1 + pyrogram/client/input_phone_contact.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 pyrogram/client/input_phone_contact.py diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 6dfc2b39..db7a3a84 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -30,4 +30,5 @@ from .client import ChatAction from .client import Client from .client import ParseMode from .client.input_media import InputMedia +from .client.input_phone_contact import InputPhoneContact from .client import Emoji diff --git a/pyrogram/client/input_phone_contact.py b/pyrogram/client/input_phone_contact.py new file mode 100644 index 00000000..d67092ff --- /dev/null +++ b/pyrogram/client/input_phone_contact.py @@ -0,0 +1,11 @@ +from pyrogram.api.types import InputPhoneContact as RawInputPhoneContact + + +class InputPhoneContact: + 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 + ) From 3ff3f5d9f4e84be1d8100423643457f5eda71622 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 20 Feb 2018 15:03:35 +0100 Subject: [PATCH 16/27] Rewrite add_contacts and delete_contacts --- pyrogram/client/client.py | 112 ++++++++------------------------------ 1 file changed, 22 insertions(+), 90 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 98cff8bb..472af427 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -45,17 +45,14 @@ from pyrogram.api.errors import ( from pyrogram.api.types import ( User, Chat, Channel, PeerUser, PeerChannel, - InputUser, InputPeerEmpty, InputPeerSelf, - InputPeerUser, InputPeerChat, InputPeerChannel, - InputPhoneContact + InputPeerUser, InputPeerChat, InputPeerChannel ) from pyrogram.crypto import AES from pyrogram.session import Auth, Session from pyrogram.session.internals import MsgId from .input_media import InputMedia from .style import Markdown, HTML -from typing import List, Union log = logging.getLogger(__name__) @@ -2310,99 +2307,34 @@ class Client: done.wait() - def add_contacts(self, - phone: Union[int, str] = None, - first_name: str = None, - last_name: str = None, - input_phone_contact_list: List[InputPhoneContact] = None): - if (phone is None or first_name is None) and \ - input_phone_contact_list is None: - log.warning("(phone and first_name) or input_phone_contact_list " - "must be not None") - return None - - if phone is not None and first_name is not None: - if str(phone)[0] != '+': - phone = '+' + str(phone) - input_phone_contact_list = [] - input_phone_contact = InputPhoneContact(client_id=0, - phone=phone, - first_name=first_name, - last_name=last_name or '') - input_phone_contact_list.append(input_phone_contact) - - # make sure that we send only InputPhoneContact - inner_input_phone_contact_list = [] - for contact in input_phone_contact_list: - if isinstance(contact, InputPhoneContact): - inner_input_phone_contact_list.append(contact) - + def add_contacts(self, contacts: list): imported_contacts = self.send( - functions.contacts.ImportContacts(inner_input_phone_contact_list)) + functions.contacts.ImportContacts( + contacts=contacts + ) + ) - for user in imported_contacts.users: - if isinstance(user, User): - if user.id in self.peers_by_id: - continue - - if user.access_hash is None: - continue - - input_peer = InputPeerUser( - user_id=user.id, - access_hash=user.access_hash - ) - - self.peers_by_id[user.id] = input_peer - - if user.username is not None: - self.peers_by_username[user.username] = input_peer + self.fetch_peers(imported_contacts.users) return imported_contacts - def delete_contacts(self, _id: int = None, - ids_list: Union[ - List[int], List[InputUser]] = None): - if _id is None and ids_list is None: - log.warning('id or ids_list must be not None') - return False + def delete_contacts(self, ids: list): + contacts = [] - contacts = self.get_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) - if _id is not None: - if not isinstance(_id, int): - log.warning('id is not int') - return False - - input_user = None - for user in contacts.users: - if isinstance(user, User): - if _id == user.id: - input_user = InputUser(user_id=user.id, - access_hash=user.access_hash) - break - - ids_list = [input_user] - - inner_ids_list = [] - for _id in ids_list: - if isinstance(_id, InputUser): - inner_ids_list.append(_id) - - if isinstance(_id, int): - input_user = None - for user in contacts.users: - if isinstance(user, User): - if _id == user.id: - input_user = InputUser( - user_id=user.id, - access_hash=user.access_hash) - break - inner_ids_list.append(input_user) - - res = self.send(functions.contacts.DeleteContacts(inner_ids_list)) - return res + return self.send( + functions.contacts.DeleteContacts( + id=contacts + ) + ) def get_contacts(self, _hash: int = 0): return self.send(functions.contacts.GetContacts(_hash)) - From c99bc9118badcf00559f12c0cdf588ef7fc3e8b0 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 20 Feb 2018 15:05:17 +0100 Subject: [PATCH 17/27] Fetch users when getting contacts --- pyrogram/client/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 472af427..7611abcd 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2337,4 +2337,7 @@ class Client: ) def get_contacts(self, _hash: int = 0): - return self.send(functions.contacts.GetContacts(_hash)) + contacts = self.send(functions.contacts.GetContacts(_hash)) + self.fetch_peers(contacts.users) + + return contacts From c33719be9e546329c18eeba32c76894f86e0275e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 20 Feb 2018 15:06:12 +0100 Subject: [PATCH 18/27] Call get_contacts when starting the client --- pyrogram/client/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 7611abcd..34fa401b 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -183,6 +183,7 @@ class Client: self.rnd_id = MsgId self.get_dialogs() + self.get_contacts() for i in range(self.UPDATES_WORKERS): Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start() From c5281eb963d0bbc9f3627dbcfa0e8a3ef5a8754b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 20 Feb 2018 15:28:01 +0100 Subject: [PATCH 19/27] Add ability to send messages to contacts by using their phone numbers --- pyrogram/client/client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 34fa401b..8f149c94 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -135,6 +135,7 @@ class Client: self.peers_by_id = {} self.peers_by_username = {} + self.peers_by_phone = {} self.channels_pts = {} @@ -225,6 +226,7 @@ class Client: continue username = entity.username + phone = entity.phone input_peer = InputPeerUser( user_id=user_id, @@ -236,6 +238,9 @@ class Client: if username is not None: self.peers_by_username[username] = input_peer + if phone is not None: + self.peers_by_phone[phone] = input_peer + if isinstance(entity, Chat): chat_id = entity.id @@ -795,6 +800,12 @@ class Client: if peer_id in ("self", "me"): return InputPeerSelf() + if peer_id.startswith("+"): + try: + return self.peers_by_phone[peer_id] + except KeyError: + raise PeerIdInvalid + peer_id = peer_id.lower().strip("@") try: From 5a44b93574a47af7ba227141af5495e98aaf1202 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 20 Feb 2018 15:48:10 +0100 Subject: [PATCH 20/27] Accept phone numbers with and without a leading "+" --- pyrogram/client/client.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 8f149c94..2548998c 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -800,19 +800,21 @@ class Client: if peer_id in ("self", "me"): return InputPeerSelf() - if peer_id.startswith("+"): + peer_id = peer_id.lower().strip("@+") + + try: + int(peer_id) + except ValueError: + try: + return self.peers_by_username[peer_id] + except KeyError: + return self.resolve_username(peer_id) + else: try: return self.peers_by_phone[peer_id] except KeyError: raise PeerIdInvalid - peer_id = peer_id.lower().strip("@") - - try: - return self.peers_by_username[peer_id] - except KeyError: - return self.resolve_username(peer_id) - if type(peer_id) is not int: if isinstance(peer_id, types.PeerUser): peer_id = peer_id.user_id From 2a9da075486480adffd6042403c4b11425737f75 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 20 Feb 2018 17:00:25 +0100 Subject: [PATCH 21/27] Catch FloodWaits when calling GetContacts --- pyrogram/client/client.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 2548998c..80deb580 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2351,7 +2351,14 @@ class Client: ) def get_contacts(self, _hash: int = 0): - contacts = self.send(functions.contacts.GetContacts(_hash)) - self.fetch_peers(contacts.users) - - return contacts + 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 From a5849e9f6d2001243224eb2e972de2d83f807aa0 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 21 Feb 2018 11:51:11 +0100 Subject: [PATCH 22/27] Add missing notice --- pyrogram/client/input_phone_contact.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pyrogram/client/input_phone_contact.py b/pyrogram/client/input_phone_contact.py index d67092ff..5a8f16b2 100644 --- a/pyrogram/client/input_phone_contact.py +++ b/pyrogram/client/input_phone_contact.py @@ -1,3 +1,21 @@ +# 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 From efbd052ec3c03b1944c805ca5a03e899c65fe152 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 21 Feb 2018 13:31:27 +0100 Subject: [PATCH 23/27] Add api_key and proxy parameter to the Client class This gives users full control on how to store and load credentials. --- pyrogram/client/client.py | 62 ++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 80deb580..2af4c3cd 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -56,7 +56,7 @@ from .style import Markdown, HTML log = logging.getLogger(__name__) -Config = namedtuple("Config", ["api_id", "api_hash"]) +ApiKey = namedtuple("ApiKey", ["api_id", "api_hash"]) Proxy = namedtuple("Proxy", ["enabled", "hostname", "port", "username", "password"]) @@ -72,6 +72,17 @@ class Client: it when you restart your script. As long as a valid session file exists, Pyrogram won't ask you again to input your phone number. + api_key (:obj:`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. + + proxy (:obj:`dict`, optional): + Your SOCKS5 Proxy settings as dict: *{hostname: str, port: int, username: str, password: str}*. + E.g.: *dict(hostname="11.22.33.44", port=1080, username="user", password="pass")*. + *username* and *password* can be omitted if your proxy doesn't require authorization. + This is an alternative way to setup a proxy if you don't want to use the *config.ini* file. + test_mode (:obj:`bool`, optional): Enable or disable log-in to testing servers. Defaults to False. Only applicable for new sessions and will be ignored in case previously @@ -109,6 +120,8 @@ class Client: def __init__(self, session_name: str, + api_key: tuple or ApiKey = None, + proxy: dict or Proxy = None, test_mode: bool = False, phone_number: str = None, phone_code: str or callable = None, @@ -117,6 +130,8 @@ class Client: last_name: str = None, workers: int = 4): self.session_name = session_name + self.api_key = api_key + self.proxy = proxy self.test_mode = test_mode self.phone_number = phone_number @@ -142,8 +157,6 @@ class Client: self.markdown = Markdown(self.peers_by_id) self.html = HTML(self.peers_by_id) - self.config = None - self.proxy = None self.session = None self.is_idle = Event() @@ -169,7 +182,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.config.api_id, + self.api_key.api_id, client=self ) @@ -520,8 +533,8 @@ class Client: r = self.send( functions.auth.SendCode( self.phone_number, - self.config.api_id, - self.config.api_hash + self.api_key.api_id, + self.api_key.api_hash ) ) except (PhoneMigrate, NetworkMigrate) as e: @@ -535,7 +548,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.config.api_id, + self.api_key.api_id, client=self ) self.session.start() @@ -543,8 +556,8 @@ class Client: r = self.send( functions.auth.SendCode( self.phone_number, - self.config.api_id, - self.config.api_hash + self.api_key.api_id, + self.api_key.api_hash ) ) break @@ -662,10 +675,16 @@ class Client: parser = ConfigParser() parser.read("config.ini") - self.config = Config( - api_id=parser.getint("pyrogram", "api_id"), - api_hash=parser.get("pyrogram", "api_hash") - ) + if parser.has_section("pyrogram"): + self.api_key = ApiKey( + api_id=parser.getint("pyrogram", "api_id"), + api_hash=parser.get("pyrogram", "api_hash") + ) + else: + self.api_key = ApiKey( + api_id=int(self.api_key[0]), + api_hash=self.api_key[1] + ) if parser.has_section("proxy"): self.proxy = Proxy( @@ -675,6 +694,15 @@ class Client: username=parser.get("proxy", "username", fallback=None) or None, password=parser.get("proxy", "password", fallback=None) or None ) + else: + if self.proxy is not None: + self.proxy = Proxy( + enabled=True, + hostname=self.proxy["hostname"], + port=int(self.proxy["port"]), + username=self.proxy.get("username", None), + password=self.proxy.get("password", None) + ) def load_session(self, session_name): try: @@ -1753,7 +1781,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.config.api_id) + session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.api_key.api_id) session.start() try: @@ -1816,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() @@ -1833,7 +1861,7 @@ class Client: self.test_mode, self.proxy, self.auth_key, - self.config.api_id + self.api_key.api_id ) session.start() @@ -1892,7 +1920,7 @@ class Client: 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 ) From 8e538199eadf0a97c1ff3c94f68acb9891451633 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 21 Feb 2018 13:34:27 +0100 Subject: [PATCH 24/27] Log info when using a proxy --- pyrogram/connection/transport/tcp/tcp.py | 5 +++++ 1 file changed, 5 insertions(+) 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) From 140a8b4e12e4c693e11c1095fa34561e343d4c31 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 21 Feb 2018 13:35:17 +0100 Subject: [PATCH 25/27] Catch RPCError in InitConnection --- pyrogram/session/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index 8978b4de..7dd0cba4 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -145,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 From 7ee8cb705934cec9940dc3940cbf2b843f426ff7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 21 Feb 2018 13:44:12 +0100 Subject: [PATCH 26/27] Document InputPhoneNumber --- pyrogram/client/input_phone_contact.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyrogram/client/input_phone_contact.py b/pyrogram/client/input_phone_contact.py index 5a8f16b2..002a3121 100644 --- a/pyrogram/client/input_phone_contact.py +++ b/pyrogram/client/input_phone_contact.py @@ -20,6 +20,20 @@ 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, From cfa19bc1c8775f985a3b1baee880a14ec3af4af0 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 21 Feb 2018 13:44:47 +0100 Subject: [PATCH 27/27] Document add_contacts and delete_contacts --- pyrogram/client/client.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 2af4c3cd..75804635 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2350,6 +2350,18 @@ class Client: 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 @@ -2361,6 +2373,19 @@ class Client: 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: