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
-
+
+
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,
)