Merge branch 'master' into new-api

# Conflicts:
#	pyrogram/crypto/aes.py
This commit is contained in:
Dan 2018-04-05 20:13:18 +02:00
commit 38e895ed82
7 changed files with 174 additions and 56 deletions

View File

@ -1,5 +1,5 @@
## Include
include COPYING COPYING.lesser NOTICE requirements.txt
include COPYING COPYING.lesser NOTICE requirements.txt requirements_extras.txt
recursive-include compiler *.py *.tl *.tsv *.txt
## Exclude

View File

@ -56,12 +56,6 @@ from .style import Markdown, HTML
log = logging.getLogger(__name__)
class APIKey:
def __init__(self, api_id: int, api_hash: str):
self.api_id = api_id
self.api_hash = api_hash
class Proxy:
def __init__(self, enabled: bool, hostname: str, port: int, username: str, password: str):
self.enabled = enabled
@ -83,10 +77,13 @@ class Client:
For Bots: pass your Bot API token, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
Note: as long as a valid User session file exists, Pyrogram won't ask you again to input your phone number.
api_key (``tuple``, optional):
Your Telegram API Key as tuple: *(api_id, api_hash)*.
E.g.: *(12345, "0123456789abcdef0123456789abcdef")*. This is an alternative way to pass it if you
don't want to use the *config.ini* file.
api_id (``int``, optional):
The *api_id* part of your Telegram API Key, as integer. E.g.: 12345
This is an alternative way to pass it if you don't want to use the *config.ini* file.
api_hash (``str``, optional):
The *api_hash* part of your Telegram API Key, as string. E.g.: "0123456789abcdef0123456789abcdef"
This is an alternative way to pass it if you don't want to use the *config.ini* file.
proxy (``dict``, optional):
Your SOCKS5 Proxy settings as dict,
@ -112,13 +109,18 @@ class Client:
Pass your Two-Step Verification password (if you have one) to avoid entering it
manually. Only applicable for new sessions.
force_sms (``str``, optional):
Pass True to force Telegram sending the authorization code via SMS.
Only applicable for new sessions.
first_name (``str``, optional):
Pass a First Name to avoid entering it manually. It will be used to automatically
create a new Telegram account in case the phone number you passed is not registered yet.
Only applicable for new sessions.
last_name (``str``, optional):
Same purpose as *first_name*; pass a Last Name to avoid entering it manually. It can
be an empty string: ""
be an empty string: "". Only applicable for new sessions.
workers (``int``, optional):
Thread pool size for handling incoming updates. Defaults to 4.
@ -132,17 +134,20 @@ class Client:
def __init__(self,
session_name: str,
api_key: tuple or APIKey = None,
api_id: int = None,
api_hash: str = None,
proxy: dict or Proxy = None,
test_mode: bool = False,
phone_number: str = None,
phone_code: str or callable = None,
password: str = None,
force_sms: bool = False,
first_name: str = None,
last_name: str = None,
workers: int = 4):
self.session_name = session_name
self.api_key = api_key
self.api_id = api_id
self.api_hash = api_hash
self.proxy = proxy
self.test_mode = test_mode
@ -151,6 +156,7 @@ class Client:
self.phone_code = phone_code
self.first_name = first_name
self.last_name = last_name
self.force_sms = force_sms
self.workers = workers
@ -189,6 +195,9 @@ class Client:
Raises:
:class:`Error <pyrogram.Error>`
"""
if self.is_started:
raise ConnectionError("Client has already been started")
if self.BOT_TOKEN_RE.match(self.session_name):
self.token = self.session_name
self.session_name = self.session_name.split(":")[0]
@ -201,7 +210,7 @@ class Client:
self.test_mode,
self.proxy,
self.auth_key,
self.api_key.api_id,
self.api_id,
client=self
)
@ -237,6 +246,9 @@ class Client:
"""Use this method to manually stop the Client.
Requires no parameters.
"""
if not self.is_started:
raise ConnectionError("Client is already stopped")
self.is_started = False
self.session.stop()
@ -254,8 +266,8 @@ class Client:
r = self.send(
functions.auth.ImportBotAuthorization(
flags=0,
api_id=self.api_key.api_id,
api_hash=self.api_key.api_hash,
api_id=self.api_id,
api_hash=self.api_hash,
bot_auth_token=self.token
)
)
@ -270,7 +282,7 @@ class Client:
self.test_mode,
self.proxy,
self.auth_key,
self.api_key.api_id,
self.api_id,
client=self
)
@ -303,8 +315,8 @@ class Client:
r = self.send(
functions.auth.SendCode(
self.phone_number,
self.api_key.api_id,
self.api_key.api_hash
self.api_id,
self.api_hash
)
)
except (PhoneMigrate, NetworkMigrate) as e:
@ -318,7 +330,7 @@ class Client:
self.test_mode,
self.proxy,
self.auth_key,
self.api_key.api_id,
self.api_id,
client=self
)
self.session.start()
@ -326,8 +338,8 @@ class Client:
r = self.send(
functions.auth.SendCode(
self.phone_number,
self.api_key.api_id,
self.api_key.api_hash
self.api_id,
self.api_hash
)
)
break
@ -348,6 +360,14 @@ class Client:
phone_registered = r.phone_registered
phone_code_hash = r.phone_code_hash
if self.force_sms:
self.send(
functions.auth.ResendCode(
phone_number=self.phone_number,
phone_code_hash=phone_code_hash
)
)
while True:
self.phone_code = (
input("Enter phone code: ") if self.phone_code is None
@ -636,7 +656,7 @@ class Client:
if not isinstance(message, types.MessageEmpty):
diff = self.send(
functions.updates.GetChannelDifference(
channel=self.resolve_peer(update.message.to_id.channel_id),
channel=self.resolve_peer(int("-100" + str(update.message.to_id.channel_id))),
filter=types.ChannelMessagesFilter(
ranges=[types.MessageRange(
min_id=update.message.id,
@ -813,32 +833,28 @@ class Client:
Raises:
:class:`Error <pyrogram.Error>`
"""
if self.is_started:
r = self.session.send(data)
if not self.is_started:
raise ConnectionError("Client has not been started")
self.fetch_peers(getattr(r, "users", []))
self.fetch_peers(getattr(r, "chats", []))
r = self.session.send(data)
return r
else:
raise ConnectionError("client '{}' is not started".format(self.session_name))
self.fetch_peers(getattr(r, "users", []))
self.fetch_peers(getattr(r, "chats", []))
return r
def load_config(self):
parser = ConfigParser()
parser.read("config.ini")
if self.api_key is not None:
self.api_key = APIKey(
api_id=int(self.api_key[0]),
api_hash=self.api_key[1]
)
elif parser.has_section("pyrogram"):
self.api_key = APIKey(
api_id=parser.getint("pyrogram", "api_id"),
api_hash=parser.get("pyrogram", "api_hash")
)
if self.api_id and self.api_hash:
pass
else:
raise AttributeError("No API Key found")
if parser.has_section("pyrogram"):
self.api_id = parser.getint("pyrogram", "api_id")
self.api_hash = parser.get("pyrogram", "api_hash")
else:
raise AttributeError("No API Key found")
if self.proxy is not None:
self.proxy = Proxy(
@ -925,6 +941,15 @@ class Client:
offset_date = parse_dialogs(dialogs)
log.info("Entities count: {}".format(len(self.peers_by_id)))
self.send(
functions.messages.GetDialogs(
0, 0, types.InputPeerEmpty(),
self.DIALOGS_AT_ONCE, True
)
)
log.info("Entities count: {}".format(len(self.peers_by_id)))
def resolve_peer(self, peer_id: int or str):
"""Use this method to get the *InputPeer* of a known *peer_id*.
@ -957,7 +982,7 @@ class Client:
except (AttributeError, binascii.Error, struct.error):
pass
peer_id = peer_id.lower().strip("@+")
peer_id = re.sub(r"[@+\s]", "", peer_id.lower())
try:
int(peer_id)
@ -2187,7 +2212,7 @@ class Client:
file_id = file_id or self.rnd_id()
md5_sum = md5() if not is_big and not is_missing_part else None
session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.api_key.api_id)
session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.api_id)
session.start()
try:
@ -2270,7 +2295,7 @@ class Client:
self.test_mode,
self.proxy,
Auth(dc_id, self.test_mode, self.proxy).create(),
self.api_key.api_id
self.api_id
)
session.start()
@ -2287,7 +2312,7 @@ class Client:
self.test_mode,
self.proxy,
self.auth_key,
self.api_key.api_id
self.api_id
)
session.start()
@ -2319,7 +2344,7 @@ class Client:
)
if isinstance(r, types.upload.File):
with tempfile.NamedTemporaryFile('wb', delete=False) as f:
with tempfile.NamedTemporaryFile("wb", delete=False) as f:
file_name = f.name
while True:
@ -2351,14 +2376,14 @@ class Client:
self.test_mode,
self.proxy,
Auth(r.dc_id, self.test_mode, self.proxy).create(),
self.api_key.api_id,
self.api_id,
is_cdn=True
)
cdn_session.start()
try:
with tempfile.NamedTemporaryFile('wb', delete=False) as f:
with tempfile.NamedTemporaryFile("wb", delete=False) as f:
file_name = f.name
while True:

View File

@ -1 +1,92 @@
# 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/>. try: import tgcrypto except ImportError as e: e.msg = ( "TgCrypto is missing and Pyrogram can't run without. " "Please install it using \"pip3 install tgcrypto\". " "More info: https://docs.pyrogram.ml/resources/TgCrypto" ) raise e class AES: @classmethod def ige_encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: return tgcrypto.ige_encrypt(data, key, iv) @classmethod def ige_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes: return tgcrypto.ige_decrypt(data, key, iv) @staticmethod def ctr_decrypt(data: bytes, key: bytes, iv: bytes, offset: int) -> bytes: replace = int.to_bytes(offset // 16, 4, "big") iv = iv[:-4] + replace return tgcrypto.ctr_decrypt(data, key, iv) @staticmethod def xor(a: bytes, b: bytes) -> bytes: return int.to_bytes( int.from_bytes(a, "big") ^ int.from_bytes(b, "big"), len(a), "big", )
# 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/>.
import logging
log = logging.getLogger(__name__)
try:
import tgcrypto
except ImportError:
log.warning(
"TgCrypto is missing! "
"Pyrogram will work the same, but at a much slower speed. "
"More info: https://docs.pyrogram.ml/resources/TgCrypto"
)
is_fast = False
import pyaes
else:
log.info("Using TgCrypto")
is_fast = True
# TODO: Ugly IFs
class AES:
@classmethod
def ige_encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
if is_fast:
return tgcrypto.ige_encrypt(data, key, iv)
else:
return cls.ige(data, key, iv, True)
@classmethod
def ige_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
if is_fast:
return tgcrypto.ige_decrypt(data, key, iv)
else:
return cls.ige(data, key, iv, False)
@staticmethod
def ctr_decrypt(data: bytes, key: bytes, iv: bytes, offset: int) -> bytes:
replace = int.to_bytes(offset // 16, 4, "big")
iv = iv[:-4] + replace
if is_fast:
return tgcrypto.ctr_decrypt(data, key, iv)
else:
ctr = pyaes.AESModeOfOperationCTR(key)
ctr._counter._counter = list(iv)
return ctr.decrypt(data)
@staticmethod
def xor(a: bytes, b: bytes) -> bytes:
return int.to_bytes(
int.from_bytes(a, "big") ^ int.from_bytes(b, "big"),
len(a),
"big",
)
@classmethod
def ige(cls, data: bytes, key: bytes, iv: bytes, encrypt: bool) -> bytes:
cipher = pyaes.AES(key)
iv_1 = iv[:16]
iv_2 = iv[16:]
data = [data[i: i + 16] for i in range(0, len(data), 16)]
if encrypt:
for i, chunk in enumerate(data):
iv_1 = data[i] = cls.xor(cipher.encrypt(cls.xor(chunk, iv_1)), iv_2)
iv_2 = chunk
else:
for i, chunk in enumerate(data):
iv_2 = data[i] = cls.xor(cipher.decrypt(cls.xor(chunk, iv_2)), iv_1)
iv_1 = chunk
return b"".join(data)

View File

@ -87,7 +87,7 @@ class Session:
test_mode: bool,
proxy: type,
auth_key: bytes,
api_id: str,
api_id: int,
is_cdn: bool = False,
client: pyrogram = None):
if not Session.notice_displayed:

View File

@ -1,2 +1,2 @@
pysocks
tgcrypto
pyaes>=1.6.1
pysocks>=1.6.8

1
requirements_extras.txt Normal file
View File

@ -0,0 +1 @@
tgcrypto>=1.0.4

View File

@ -25,8 +25,8 @@ from compiler.api import compiler as api_compiler
from compiler.error import compiler as error_compiler
def requirements():
with open("requirements.txt", encoding="utf-8") as r:
def read(file: str) -> list:
with open(file, encoding="utf-8") as r:
return [i.strip() for i in r]
@ -82,5 +82,6 @@ setup(
python_requires="~=3.4",
packages=find_packages(exclude=["compiler*"]),
zip_safe=False,
install_requires=requirements()
install_requires=read("requirements.txt"),
extras_require={"tgcrypto": read("requirements_extras.txt")}
)