2
0
mirror of https://github.com/pyrogram/pyrogram synced 2025-08-29 13:27:47 +00:00

Merge branch 'master' into docs

This commit is contained in:
Dan 2018-02-21 13:44:58 +01:00
commit 6a5810134b
12 changed files with 364 additions and 78 deletions

View File

@ -81,6 +81,12 @@ Installation
$ pip install --upgrade pyrogram $ pip install --upgrade pyrogram
- Or, with TgCrypto_:
.. code:: shell
$ pip install --upgrade pyrogram[tgcrypto]
Configuration Configuration
------------- -------------
@ -168,7 +174,7 @@ License
.. _`Email`: admin@pyrogram.ml .. _`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 .. _`GNU Lesser General Public License v3 or later (LGPLv3+)`: COPYING.lesser
@ -195,8 +201,8 @@ License
<a href="https://t.me/PyrogramChat"> <a href="https://t.me/PyrogramChat">
Community Community
</a </a>
<br><br><br> <br><br>
<a href="compiler/api/source/main_api.tl"> <a href="compiler/api/source/main_api.tl">
<img src="https://www.pyrogram.ml/images/scheme.svg" <img src="https://www.pyrogram.ml/images/scheme.svg"
alt="Scheme Layer 75"> alt="Scheme Layer 75">

View File

@ -23,11 +23,12 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance
"e" if sys.getfilesystemencoding() == "ascii" else "\xe8" "e" if sys.getfilesystemencoding() == "ascii" else "\xe8"
) )
__license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)" __license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
__version__ = "0.6.0" __version__ = "0.6.1"
from .api.errors import Error from .api.errors import Error
from .client import ChatAction from .client import ChatAction
from .client import Client from .client import Client
from .client import ParseMode from .client import ParseMode
from .client.input_media import InputMedia from .client.input_media import InputMedia
from .client.input_phone_contact import InputPhoneContact
from .client import Emoji from .client import Emoji

View File

@ -27,6 +27,7 @@ import threading
import time import time
from collections import namedtuple from collections import namedtuple
from configparser import ConfigParser from configparser import ConfigParser
from datetime import datetime
from hashlib import sha256, md5 from hashlib import sha256, md5
from queue import Queue from queue import Queue
from signal import signal, SIGINT, SIGTERM, SIGABRT from signal import signal, SIGINT, SIGTERM, SIGABRT
@ -39,8 +40,8 @@ from pyrogram.api.errors import (
PhoneNumberUnoccupied, PhoneCodeInvalid, PhoneCodeHashEmpty, PhoneNumberUnoccupied, PhoneCodeInvalid, PhoneCodeHashEmpty,
PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded, PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded,
PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing, PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing,
ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned,
) VolumeLocNotFound)
from pyrogram.api.types import ( from pyrogram.api.types import (
User, Chat, Channel, User, Chat, Channel,
PeerUser, PeerChannel, PeerUser, PeerChannel,
@ -49,12 +50,13 @@ from pyrogram.api.types import (
) )
from pyrogram.crypto import AES from pyrogram.crypto import AES
from pyrogram.session import Auth, Session from pyrogram.session import Auth, Session
from pyrogram.session.internals import MsgId
from .input_media import InputMedia from .input_media import InputMedia
from .style import Markdown, HTML from .style import Markdown, HTML
log = logging.getLogger(__name__) 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"]) Proxy = namedtuple("Proxy", ["enabled", "hostname", "port", "username", "password"])
@ -70,6 +72,17 @@ class Client:
it when you restart your script. As long as a valid session file exists, 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. 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): test_mode (:obj:`bool`, optional):
Enable or disable log-in to testing servers. Defaults to False. Enable or disable log-in to testing servers. Defaults to False.
Only applicable for new sessions and will be ignored in case previously Only applicable for new sessions and will be ignored in case previously
@ -103,9 +116,12 @@ class Client:
INVITE_LINK_RE = re.compile(r"^(?:https?://)?t\.me/joinchat/(.+)$") INVITE_LINK_RE = re.compile(r"^(?:https?://)?t\.me/joinchat/(.+)$")
DIALOGS_AT_ONCE = 100 DIALOGS_AT_ONCE = 100
UPDATES_WORKERS = 2 UPDATES_WORKERS = 2
DOWNLOAD_WORKERS = 1
def __init__(self, def __init__(self,
session_name: str, session_name: str,
api_key: tuple or ApiKey = None,
proxy: dict or Proxy = None,
test_mode: bool = False, test_mode: bool = False,
phone_number: str = None, phone_number: str = None,
phone_code: str or callable = None, phone_code: str or callable = None,
@ -114,6 +130,8 @@ class Client:
last_name: str = None, last_name: str = None,
workers: int = 4): workers: int = 4):
self.session_name = session_name self.session_name = session_name
self.api_key = api_key
self.proxy = proxy
self.test_mode = test_mode self.test_mode = test_mode
self.phone_number = phone_number self.phone_number = phone_number
@ -132,14 +150,13 @@ class Client:
self.peers_by_id = {} self.peers_by_id = {}
self.peers_by_username = {} self.peers_by_username = {}
self.peers_by_phone = {}
self.channels_pts = {} self.channels_pts = {}
self.markdown = Markdown(self.peers_by_id) self.markdown = Markdown(self.peers_by_id)
self.html = HTML(self.peers_by_id) self.html = HTML(self.peers_by_id)
self.config = None
self.proxy = None
self.session = None self.session = None
self.is_idle = Event() self.is_idle = Event()
@ -148,6 +165,8 @@ class Client:
self.update_queue = Queue() self.update_queue = Queue()
self.update_handler = None self.update_handler = None
self.download_queue = Queue()
def start(self): def start(self):
"""Use this method to start the Client after creating it. """Use this method to start the Client after creating it.
Requires no parameters. Requires no parameters.
@ -163,7 +182,7 @@ class Client:
self.test_mode, self.test_mode,
self.proxy, self.proxy,
self.auth_key, self.auth_key,
self.config.api_id, self.api_key.api_id,
client=self client=self
) )
@ -176,8 +195,9 @@ class Client:
self.password = None self.password = None
self.save_session() self.save_session()
self.rnd_id = self.session.msg_id self.rnd_id = MsgId
self.get_dialogs() self.get_dialogs()
self.get_contacts()
for i in range(self.UPDATES_WORKERS): for i in range(self.UPDATES_WORKERS):
Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start() Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start()
@ -185,6 +205,9 @@ class Client:
for i in range(self.workers): for i in range(self.workers):
Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start() 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() mimetypes.init()
def stop(self): def stop(self):
@ -199,6 +222,9 @@ class Client:
for _ in range(self.workers): for _ in range(self.workers):
self.update_queue.put(None) self.update_queue.put(None)
for _ in range(self.DOWNLOAD_WORKERS):
self.download_queue.put(None)
def fetch_peers(self, entities: list): def fetch_peers(self, entities: list):
for entity in entities: for entity in entities:
if isinstance(entity, User): if isinstance(entity, User):
@ -213,6 +239,7 @@ class Client:
continue continue
username = entity.username username = entity.username
phone = entity.phone
input_peer = InputPeerUser( input_peer = InputPeerUser(
user_id=user_id, user_id=user_id,
@ -224,6 +251,9 @@ class Client:
if username is not None: if username is not None:
self.peers_by_username[username] = input_peer self.peers_by_username[username] = input_peer
if phone is not None:
self.peers_by_phone[phone] = input_peer
if isinstance(entity, Chat): if isinstance(entity, Chat):
chat_id = entity.id chat_id = entity.id
@ -260,6 +290,62 @@ class Client:
if username is not None: if username is not None:
self.peers_by_username[username] = input_peer self.peers_by_username[username] = input_peer
def download_worker(self):
name = threading.current_thread().name
log.debug("{} started".format(name))
while True:
media = self.download_queue.get()
if media is None:
break
media, file_name, done = media
try:
if isinstance(media, types.MessageMediaDocument):
document = media.document
if isinstance(document, types.Document):
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")
tmp_file_name = self.get_file(
dc_id=document.dc_id,
id=document.id,
access_hash=document.access_hash,
version=document.version
)
try:
os.remove("./downloads/{}".format(file_name))
except FileNotFoundError:
pass
os.renames("./{}".format(tmp_file_name), "./downloads/{}".format(file_name))
except Exception as e:
log.error(e, exc_info=True)
finally:
done.set()
log.debug("{} stopped".format(name))
def updates_worker(self): def updates_worker(self):
name = threading.current_thread().name name = threading.current_thread().name
log.debug("{} started".format(name)) log.debug("{} started".format(name))
@ -447,8 +533,8 @@ class Client:
r = self.send( r = self.send(
functions.auth.SendCode( functions.auth.SendCode(
self.phone_number, self.phone_number,
self.config.api_id, self.api_key.api_id,
self.config.api_hash self.api_key.api_hash
) )
) )
except (PhoneMigrate, NetworkMigrate) as e: except (PhoneMigrate, NetworkMigrate) as e:
@ -462,7 +548,7 @@ class Client:
self.test_mode, self.test_mode,
self.proxy, self.proxy,
self.auth_key, self.auth_key,
self.config.api_id, self.api_key.api_id,
client=self client=self
) )
self.session.start() self.session.start()
@ -470,8 +556,8 @@ class Client:
r = self.send( r = self.send(
functions.auth.SendCode( functions.auth.SendCode(
self.phone_number, self.phone_number,
self.config.api_id, self.api_key.api_id,
self.config.api_hash self.api_key.api_hash
) )
) )
break break
@ -589,10 +675,16 @@ class Client:
parser = ConfigParser() parser = ConfigParser()
parser.read("config.ini") parser.read("config.ini")
self.config = Config( if parser.has_section("pyrogram"):
self.api_key = ApiKey(
api_id=parser.getint("pyrogram", "api_id"), api_id=parser.getint("pyrogram", "api_id"),
api_hash=parser.get("pyrogram", "api_hash") 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"): if parser.has_section("proxy"):
self.proxy = Proxy( self.proxy = Proxy(
@ -602,6 +694,15 @@ class Client:
username=parser.get("proxy", "username", fallback=None) or None, username=parser.get("proxy", "username", fallback=None) or None,
password=parser.get("proxy", "password", 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): def load_session(self, session_name):
try: try:
@ -727,12 +828,20 @@ class Client:
if peer_id in ("self", "me"): if peer_id in ("self", "me"):
return InputPeerSelf() return InputPeerSelf()
peer_id = peer_id.lower().strip("@") peer_id = peer_id.lower().strip("@+")
try:
int(peer_id)
except ValueError:
try: try:
return self.peers_by_username[peer_id] return self.peers_by_username[peer_id]
except KeyError: except KeyError:
return self.resolve_username(peer_id) return self.resolve_username(peer_id)
else:
try:
return self.peers_by_phone[peer_id]
except KeyError:
raise PeerIdInvalid
if type(peer_id) is not int: if type(peer_id) is not int:
if isinstance(peer_id, types.PeerUser): if isinstance(peer_id, types.PeerUser):
@ -1667,13 +1776,12 @@ class Client:
part_size = 512 * 1024 part_size = 512 * 1024
file_size = os.path.getsize(path) file_size = os.path.getsize(path)
file_total_parts = math.ceil(file_size / part_size) file_total_parts = math.ceil(file_size / part_size)
# is_big = True if file_size > 10 * 1024 * 1024 else False 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_missing_part = True if file_id is not None else False is_missing_part = True if file_id is not None else False
file_id = file_id or self.rnd_id() file_id = file_id or self.rnd_id()
md5_sum = md5() if not is_big and not is_missing_part else None 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() session.start()
try: try:
@ -1723,11 +1831,7 @@ class Client:
volume_id: int = None, volume_id: int = None,
local_id: int = None, local_id: int = None,
secret: int = None, secret: int = None,
version: int = 0): version: int = 0) -> str:
# TODO: Refine
# TODO: Use proper file name and extension
# TODO: Remove redundant code
if dc_id != self.dc_id: if dc_id != self.dc_id:
exported_auth = self.send( exported_auth = self.send(
functions.auth.ExportAuthorization( functions.auth.ExportAuthorization(
@ -1740,7 +1844,7 @@ class Client:
self.test_mode, self.test_mode,
self.proxy, self.proxy,
Auth(dc_id, self.test_mode, self.proxy).create(), Auth(dc_id, self.test_mode, self.proxy).create(),
self.config.api_id self.api_key.api_id
) )
session.start() session.start()
@ -1757,7 +1861,7 @@ class Client:
self.test_mode, self.test_mode,
self.proxy, self.proxy,
self.auth_key, self.auth_key,
self.config.api_id self.api_key.api_id
) )
session.start() session.start()
@ -1775,7 +1879,8 @@ class Client:
version=version version=version
) )
limit = 512 * 1024 file_name = str(MsgId())
limit = 1024 * 1024
offset = 0 offset = 0
try: try:
@ -1788,7 +1893,7 @@ class Client:
) )
if isinstance(r, types.upload.File): 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: while True:
chunk = r.bytes chunk = r.bytes
@ -1796,6 +1901,9 @@ class Client:
break break
f.write(chunk) f.write(chunk)
f.flush()
os.fsync(f.fileno())
offset += limit offset += limit
r = session.send( r = session.send(
@ -1805,20 +1913,21 @@ class Client:
limit=limit limit=limit
) )
) )
if isinstance(r, types.upload.FileCdnRedirect): if isinstance(r, types.upload.FileCdnRedirect):
cdn_session = Session( cdn_session = Session(
r.dc_id, r.dc_id,
self.test_mode, self.test_mode,
self.proxy, self.proxy,
Auth(r.dc_id, self.test_mode, self.proxy).create(), Auth(r.dc_id, self.test_mode, self.proxy).create(),
self.config.api_id, self.api_key.api_id,
is_cdn=True is_cdn=True
) )
cdn_session.start() cdn_session.start()
try: try:
with open("_".join([str(id), str(access_hash), str(version)]) + ".jpg", "wb") as f: with open(file_name, "wb") as f:
while True: while True:
r2 = cdn_session.send( r2 = cdn_session.send(
functions.upload.GetCdnFile( functions.upload.GetCdnFile(
@ -1830,26 +1939,47 @@ class Client:
) )
if isinstance(r2, types.upload.CdnFileReuploadNeeded): if isinstance(r2, types.upload.CdnFileReuploadNeeded):
try:
session.send( session.send(
functions.upload.ReuploadCdnFile( functions.upload.ReuploadCdnFile(
file_token=r.file_token, file_token=r.file_token,
request_token=r2.request_token request_token=r2.request_token
) )
) )
except VolumeLocNotFound:
break
else:
continue continue
elif isinstance(r2, types.upload.CdnFile):
chunk = r2.bytes chunk = r2.bytes
if not chunk:
break
# https://core.telegram.org/cdn#decrypting-files # https://core.telegram.org/cdn#decrypting-files
decrypted_chunk = AES.ctr_decrypt(chunk, r.encryption_key, r.encryption_iv, offset) decrypted_chunk = AES.ctr_decrypt(
chunk,
r.encryption_key,
r.encryption_iv,
offset
)
# TODO: https://core.telegram.org/cdn#verifying-files hashes = session.send(
# TODO: Save to temp file, flush each chunk, rename to full if everything is ok 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.write(decrypted_chunk)
f.flush()
os.fsync(f.fileno())
if len(chunk) < limit:
break
offset += limit offset += limit
except Exception as e: except Exception as e:
log.error(e) log.error(e)
@ -1858,7 +1988,7 @@ class Client:
except Exception as e: except Exception as e:
log.error(e) log.error(e)
else: else:
return True return file_name
finally: finally:
session.stop() session.stop()
@ -2210,3 +2340,78 @@ class Client:
reply_to_msg_id=reply_to_message_id 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 <pyrogram.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

View File

@ -1,3 +1,22 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# 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 <http://www.gnu.org/licenses/>.
class Emoji: class Emoji:
HELMET_WITH_WHITE_CROSS_TYPE_1_2 = "\u26d1\U0001f3fb" HELMET_WITH_WHITE_CROSS_TYPE_1_2 = "\u26d1\U0001f3fb"
HELMET_WITH_WHITE_CROSS_TYPE_3 = "\u26d1\U0001f3fc" HELMET_WITH_WHITE_CROSS_TYPE_3 = "\u26d1\U0001f3fc"

View File

@ -0,0 +1,43 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# 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 <http://www.gnu.org/licenses/>.
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
)

View File

@ -44,6 +44,11 @@ class TCP(socks.socksocket):
password=proxy.password password=proxy.password
) )
log.info("Using proxy {}:{}".format(
proxy.hostname,
proxy.port
))
def close(self): def close(self):
try: try:
self.shutdown(socket.SHUT_RDWR) self.shutdown(socket.SHUT_RDWR)

View File

@ -23,7 +23,7 @@ log = logging.getLogger(__name__)
try: try:
import tgcrypto import tgcrypto
except ImportError: except ImportError:
logging.warning( log.warning(
"TgCrypto is missing! " "TgCrypto is missing! "
"Pyrogram will work the same, but at a much slower speed. " "Pyrogram will work the same, but at a much slower speed. "
"More info: https://docs.pyrogram.ml/resources/TgCrypto" "More info: https://docs.pyrogram.ml/resources/TgCrypto"

View File

@ -51,12 +51,12 @@ class Auth:
self.test_mode = test_mode self.test_mode = test_mode
self.connection = Connection(DataCenter(dc_id, test_mode), proxy) 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 ( return (
bytes(8) bytes(8)
+ Long(self.msg_id()) + Long(MsgId())
+ Int(len(data.write())) + Int(len(data.write()))
+ data.write() + data.write()
) )

View File

@ -26,14 +26,13 @@ not_content_related = [Ping, HttpWait, MsgsAck, MsgContainer]
class MsgFactory: class MsgFactory:
def __init__(self, msg_id: MsgId): def __init__(self):
self.msg_id = msg_id
self.seq_no = SeqNo() self.seq_no = SeqNo()
def __call__(self, body: Object) -> Message: def __call__(self, body: Object) -> Message:
return Message( return Message(
body, body,
self.msg_id(), MsgId(),
self.seq_no(type(body) not in not_content_related), self.seq_no(type(body) not in not_content_related),
len(body) len(body)
) )

View File

@ -21,17 +21,15 @@ from time import time
class MsgId: class MsgId:
def __init__(self, delta_time: float = 0.0): last_time = 0
self.delta_time = delta_time offset = 0
self.last_time = 0 lock = Lock()
self.offset = 0
self.lock = Lock()
def __call__(self) -> int: def __new__(cls) -> int:
with self.lock: with cls.lock:
now = time() now = time()
self.offset = self.offset + 4 if now == self.last_time else 0 cls.offset = cls.offset + 4 if now == cls.last_time else 0
msg_id = int((now + self.delta_time) * 2 ** 32) + self.offset msg_id = int(now * 2 ** 32) + cls.offset
self.last_time = now cls.last_time = now
return msg_id return msg_id

View File

@ -89,9 +89,8 @@ class Session:
self.auth_key = auth_key self.auth_key = auth_key
self.auth_key_id = sha1(auth_key).digest()[-8:] self.auth_key_id = sha1(auth_key).digest()[-8:]
self.msg_id = MsgId() self.session_id = Long(MsgId())
self.session_id = Long(self.msg_id()) self.msg_factory = MsgFactory()
self.msg_factory = MsgFactory(self.msg_id)
self.current_salt = None self.current_salt = None
@ -146,7 +145,7 @@ class Session:
self.ping_thread.start() self.ping_thread.start()
log.info("Connection inited: Layer {}".format(layer)) log.info("Connection inited: Layer {}".format(layer))
except (OSError, TimeoutError): except (OSError, TimeoutError, Error):
self.stop() self.stop()
else: else:
break break
@ -338,7 +337,10 @@ class Session:
while True: while True:
packet = self.connection.recv() 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(): if self.is_connected.is_set():
Thread(target=self.restart, name="RestartThread").start() Thread(target=self.restart, name="RestartThread").start()
break break

View File

@ -66,6 +66,14 @@ setup(
], ],
packages=find_packages(), packages=find_packages(),
zip_safe=False, zip_safe=False,
install_requires=["pyaes", "pysocks"], install_requires=[
"pyaes",
"pysocks"
],
extras_require={
"tgcrypto": [
"tgcrypto"
]
},
include_package_data=True, include_package_data=True,
) )