mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-25 19:03:46 +03:00
First attempt at TelegramClient. Added fixes and doc
This commit is contained in:
parent
c863537b7b
commit
39a23559f0
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -3,6 +3,10 @@ tl/functions/
|
||||||
tl/types/
|
tl/types/
|
||||||
tl/all_tlobjects.py
|
tl/all_tlobjects.py
|
||||||
|
|
||||||
|
# User session
|
||||||
|
*.session
|
||||||
|
api/settings
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
11
README.md
11
README.md
|
@ -11,9 +11,14 @@ This project requires the following Python modules, which can be installed by is
|
||||||
Linux terminal:
|
Linux terminal:
|
||||||
- `pyaes` ([GitHub](https://github.com/ricmoo/pyaes), [package index](https://pypi.python.org/pypi/pyaes))
|
- `pyaes` ([GitHub](https://github.com/ricmoo/pyaes), [package index](https://pypi.python.org/pypi/pyaes))
|
||||||
|
|
||||||
### We need your help!
|
Also, you need to obtain your both [API ID and Hash](my.telegram.org). Once you have them, head to `api/` and create a copy of
|
||||||
As of now, the project is fully **untested** and with many pending things to do. If you know both Python and C#, please don't
|
the `settings_example` file, naming it `settings` (lowercase!). Then fill the file with the corresponding values (your `api_id`,
|
||||||
think it twice and help us (me)!
|
`api_hash` and phone number in international format). Now it is when you're ready to go!
|
||||||
|
|
||||||
|
### Plans for the future
|
||||||
|
If everything works well, this probably ends up being a Python package :)
|
||||||
|
|
||||||
|
But as of now, and until that happens, help is highly appreciated!
|
||||||
|
|
||||||
### Code generator limitations
|
### Code generator limitations
|
||||||
The current code generator is not complete, yet adding the missing features would only over-complicate an already hard-to-read code.
|
The current code generator is not complete, yet adding the missing features would only over-complicate an already hard-to-read code.
|
||||||
|
|
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
4
api/settings_example
Normal file
4
api/settings_example
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
api_id=12345
|
||||||
|
api_hash=0123456789abcdef0123456789abcdef
|
||||||
|
user_phone=34600000000
|
||||||
|
session_name=anonymous
|
0
crypto/__init__.py
Normal file
0
crypto/__init__.py
Normal file
|
@ -1,9 +1,12 @@
|
||||||
|
# This file is based on TLSharp
|
||||||
|
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/MTProto/Crypto/AES.cs
|
||||||
import pyaes
|
import pyaes
|
||||||
|
|
||||||
|
|
||||||
class AES:
|
class AES:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decrypt_ige(cipher_text, key, iv):
|
def decrypt_ige(cipher_text, key, iv):
|
||||||
|
"""Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector"""
|
||||||
iv1 = iv[:len(iv)//2]
|
iv1 = iv[:len(iv)//2]
|
||||||
iv2 = iv[len(iv)//2:]
|
iv2 = iv[len(iv)//2:]
|
||||||
|
|
||||||
|
@ -31,6 +34,7 @@ class AES:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encrypt_ige(plain_text, key, iv):
|
def encrypt_ige(plain_text, key, iv):
|
||||||
|
"""Encrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector"""
|
||||||
# TODO: Random padding
|
# TODO: Random padding
|
||||||
padding = bytes(16 - len(plain_text) % 16)
|
padding = bytes(16 - len(plain_text) % 16)
|
||||||
plain_text += padding
|
plain_text += padding
|
|
@ -20,6 +20,7 @@ class AuthKey:
|
||||||
self.key_id = reader.read_long(signed=False)
|
self.key_id = reader.read_long(signed=False)
|
||||||
|
|
||||||
def calc_new_nonce_hash(self, new_nonce, number):
|
def calc_new_nonce_hash(self, new_nonce, number):
|
||||||
|
"""Calculates the new nonce hash based on the current class fields' values"""
|
||||||
with BinaryWriter() as writer:
|
with BinaryWriter() as writer:
|
||||||
writer.write(new_nonce)
|
writer.write(new_nonce)
|
||||||
writer.write_byte(number)
|
writer.write_byte(number)
|
|
@ -6,6 +6,7 @@ from random import randint
|
||||||
class Factorizator:
|
class Factorizator:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_small_multiplier_lopatin(what):
|
def find_small_multiplier_lopatin(what):
|
||||||
|
"""Finds the small multiplier by using Lopatin's method"""
|
||||||
g = 0
|
g = 0
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
q = (randint(0, 127) & 15) + 17
|
q = (randint(0, 127) & 15) + 17
|
||||||
|
@ -41,6 +42,7 @@ class Factorizator:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def gcd(a, b):
|
def gcd(a, b):
|
||||||
|
"""Calculates the greatest common divisor"""
|
||||||
while a != 0 and b != 0:
|
while a != 0 and b != 0:
|
||||||
while b & 1 == 0:
|
while b & 1 == 0:
|
||||||
b >>= 1
|
b >>= 1
|
||||||
|
@ -57,5 +59,6 @@ class Factorizator:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def factorize(pq):
|
def factorize(pq):
|
||||||
|
"""Factorizes the given number and returns both the divisor and the number divided by the divisor"""
|
||||||
divisor = Factorizator.find_small_multiplier_lopatin(pq)
|
divisor = Factorizator.find_small_multiplier_lopatin(pq)
|
||||||
return divisor, pq // divisor
|
return divisor, pq // divisor
|
|
@ -1,5 +1,5 @@
|
||||||
# This file is based on TLSharp
|
# This file is based on TLSharp
|
||||||
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Auth/Authenticator.cs
|
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/MTProto/Crypto/RSA.cs
|
||||||
from utils.binary_writer import BinaryWriter
|
from utils.binary_writer import BinaryWriter
|
||||||
import utils.helpers as utils
|
import utils.helpers as utils
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ class RSAServerKey:
|
||||||
self.e = e
|
self.e = e
|
||||||
|
|
||||||
def encrypt(self, data, offset=None, length=None):
|
def encrypt(self, data, offset=None, length=None):
|
||||||
|
"""Encrypts the given data with the current key"""
|
||||||
if offset is None:
|
if offset is None:
|
||||||
offset = 0
|
offset = 0
|
||||||
if length is None:
|
if length is None:
|
||||||
|
@ -37,8 +38,6 @@ class RSAServerKey:
|
||||||
return padding + cipher_text
|
return padding + cipher_text
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RSA:
|
class RSA:
|
||||||
_server_keys = {
|
_server_keys = {
|
||||||
'216be86c022bb4c3':
|
'216be86c022bb4c3':
|
||||||
|
@ -55,6 +54,7 @@ class RSA:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encrypt(fingerprint, data, offset=None, length=None):
|
def encrypt(fingerprint, data, offset=None, length=None):
|
||||||
|
"""Encrypts the given data given a fingerprint"""
|
||||||
if fingerprint.lower() not in RSA._server_keys:
|
if fingerprint.lower() not in RSA._server_keys:
|
||||||
return None
|
return None
|
||||||
|
|
30
main.py
30
main.py
|
@ -1,17 +1,21 @@
|
||||||
import parser.tl_generator
|
import tl_generator
|
||||||
|
from tl.telegram_client import TelegramClient
|
||||||
|
from utils.helpers import load_settings
|
||||||
|
|
||||||
from network.tcp_transport import TcpTransport
|
|
||||||
from network.authenticator import do_authentication
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if not parser.tl_generator.tlobjects_exist():
|
if not tl_generator.tlobjects_exist():
|
||||||
print('First run. Generating TLObjects...')
|
print('Please run `python3 tl_generator.py` first!')
|
||||||
parser.tl_generator.generate_tlobjects('scheme.tl')
|
|
||||||
print('Done.')
|
|
||||||
|
|
||||||
transport = TcpTransport('149.154.167.91', 443)
|
else:
|
||||||
auth_key, time_offset = do_authentication(transport)
|
settings = load_settings()
|
||||||
print(auth_key.aux_hash)
|
client = TelegramClient(session_user_id=settings.get('session_name', 'anonymous'),
|
||||||
print(auth_key.key)
|
layer=54,
|
||||||
print(auth_key.key_id)
|
api_id=settings['api_id'],
|
||||||
print(time_offset)
|
api_hash=settings['api_hash'])
|
||||||
|
|
||||||
|
client.connect()
|
||||||
|
if not client.is_user_authorized():
|
||||||
|
phone_code_hash = client.send_code_request(settings['user_phone'])
|
||||||
|
code = input('Enter the code you just received: ')
|
||||||
|
client.make_auth(settings['user_phone'], phone_code_hash, code)
|
||||||
|
|
|
@ -4,18 +4,21 @@
|
||||||
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Auth/Step2_DHExchange.cs
|
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Auth/Step2_DHExchange.cs
|
||||||
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Auth/Step3_CompleteDHExchange.cs
|
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Auth/Step3_CompleteDHExchange.cs
|
||||||
|
|
||||||
from network.mtproto_plain_sender import MtProtoPlainSender
|
|
||||||
from utils.binary_writer import BinaryWriter
|
|
||||||
from utils.binary_reader import BinaryReader
|
|
||||||
from utils.factorizator import Factorizator
|
|
||||||
from utils.auth_key import AuthKey
|
|
||||||
import utils.helpers as utils
|
|
||||||
import time
|
import time
|
||||||
from utils.rsa import RSA
|
|
||||||
from utils.aes import AES
|
import utils.helpers as utils
|
||||||
|
from crypto.aes import AES
|
||||||
|
from crypto.auth_key import AuthKey
|
||||||
|
from crypto.factorizator import Factorizator
|
||||||
|
from crypto.rsa import RSA
|
||||||
|
from network.mtproto_plain_sender import MtProtoPlainSender
|
||||||
|
from utils.binary_reader import BinaryReader
|
||||||
|
from utils.binary_writer import BinaryWriter
|
||||||
|
|
||||||
|
|
||||||
def do_authentication(transport):
|
def do_authentication(transport):
|
||||||
|
"""Executes the authentication process with the Telegram servers.
|
||||||
|
If no error is rose, returns both the authorization key and the time offset"""
|
||||||
sender = MtProtoPlainSender(transport)
|
sender = MtProtoPlainSender(transport)
|
||||||
|
|
||||||
# Step 1 sending: PQ Request
|
# Step 1 sending: PQ Request
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/MtProtoSender.cs
|
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/MtProtoSender.cs
|
||||||
import re
|
import re
|
||||||
import zlib
|
import zlib
|
||||||
import pyaes
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
from crypto.aes import AES
|
||||||
from utils.binary_writer import BinaryWriter
|
from utils.binary_writer import BinaryWriter
|
||||||
from utils.binary_reader import BinaryReader
|
from utils.binary_reader import BinaryReader
|
||||||
from tl.types.msgs_ack import MsgsAck
|
from tl.types.msgs_ack import MsgsAck
|
||||||
|
@ -76,13 +76,12 @@ class MtProtoSender:
|
||||||
|
|
||||||
msg_key = helpers.calc_msg_key(writer.get_bytes())
|
msg_key = helpers.calc_msg_key(writer.get_bytes())
|
||||||
|
|
||||||
key, iv = helpers.calc_key(self.session.auth_key.data, msg_key, True)
|
key, iv = helpers.calc_key(self.session.auth_key.key, msg_key, True)
|
||||||
aes = pyaes.AESModeOfOperationCFB(key, iv, 16)
|
cipher_text = AES.encrypt_ige(writer.get_bytes(), key, iv)
|
||||||
cipher_text = aes.encrypt(writer.get_bytes())
|
|
||||||
|
|
||||||
# And then finally send the packet
|
# And then finally send the packet
|
||||||
with BinaryWriter() as writer:
|
with BinaryWriter() as writer:
|
||||||
writer.write_long(self.session.auth_key.id, signed=False)
|
writer.write_long(self.session.auth_key.key_id, signed=False)
|
||||||
writer.write(msg_key)
|
writer.write(msg_key)
|
||||||
writer.write(cipher_text)
|
writer.write(cipher_text)
|
||||||
|
|
||||||
|
@ -96,15 +95,14 @@ class MtProtoSender:
|
||||||
|
|
||||||
with BinaryReader(body) as reader:
|
with BinaryReader(body) as reader:
|
||||||
if len(body) < 8:
|
if len(body) < 8:
|
||||||
raise BufferError("Can't decode packet")
|
raise BufferError("Can't decode packet ({})".format(body))
|
||||||
|
|
||||||
# TODO Check for both auth key ID and msg_key correctness
|
# TODO Check for both auth key ID and msg_key correctness
|
||||||
remote_auth_key_id = reader.read_long()
|
remote_auth_key_id = reader.read_long()
|
||||||
msg_key = reader.read(16)
|
msg_key = reader.read(16)
|
||||||
|
|
||||||
key, iv = helpers.calc_key(self.session.auth_key.data, msg_key, False)
|
key, iv = helpers.calc_key(self.session.auth_key.data, msg_key, False)
|
||||||
aes = pyaes.AESModeOfOperationCFB(key, iv, 16)
|
plain_text = AES.decrypt_ige(reader.read(len(body) - reader.tell_position()), key, iv)
|
||||||
plain_text = aes.decrypt(reader.read(len(body) - reader.tell_position()))
|
|
||||||
|
|
||||||
with BinaryReader(plain_text) as plain_text_reader:
|
with BinaryReader(plain_text) as plain_text_reader:
|
||||||
remote_salt = plain_text_reader.read_long()
|
remote_salt = plain_text_reader.read_long()
|
||||||
|
@ -278,7 +276,7 @@ class MtProtoSender:
|
||||||
|
|
||||||
elif error_msg.startswith('PHONE_MIGRATE_'):
|
elif error_msg.startswith('PHONE_MIGRATE_'):
|
||||||
dc_index = int(re.search(r'\d+', error_msg).group(0))
|
dc_index = int(re.search(r'\d+', error_msg).group(0))
|
||||||
raise ConnectionError('Your phone number registered to {} dc. Please update settings. '
|
raise ConnectionError('Your phone number is registered to {} DC. Please update settings. '
|
||||||
'See https://github.com/sochix/TLSharp#i-get-an-error-migrate_x '
|
'See https://github.com/sochix/TLSharp#i-get-an-error-migrate_x '
|
||||||
'for details.'.format(dc_index))
|
'for details.'.format(dc_index))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -17,7 +17,6 @@ class Session:
|
||||||
self.salt = 0 # Unsigned long
|
self.salt = 0 # Unsigned long
|
||||||
self.time_offset = 0
|
self.time_offset = 0
|
||||||
self.last_message_id = 0 # Long
|
self.last_message_id = 0 # Long
|
||||||
self.session_expires = 0
|
|
||||||
self.user = None
|
self.user = None
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
@ -26,13 +25,13 @@ class Session:
|
||||||
pickle.dump(self, file)
|
pickle.dump(self, file)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def try_load_or_create_new(self, session_user_id):
|
def try_load_or_create_new(session_user_id):
|
||||||
"""Loads a saved session_user_id session, or creates a new one if none existed before"""
|
"""Loads a saved session_user_id session, or creates a new one if none existed before"""
|
||||||
filepath = '{}.session'.format(self.session_user_id)
|
filepath = '{}.session'.format(session_user_id)
|
||||||
|
|
||||||
if file_exists(filepath):
|
if file_exists(filepath):
|
||||||
with open(filepath, 'rb') as file:
|
with open(filepath, 'rb') as file:
|
||||||
return pickle.load(self)
|
return pickle.load(file)
|
||||||
else:
|
else:
|
||||||
return Session(session_user_id)
|
return Session(session_user_id)
|
||||||
|
|
||||||
|
@ -40,7 +39,7 @@ class Session:
|
||||||
"""Generates a new message ID based on the current time (in ms) since epoch"""
|
"""Generates a new message ID based on the current time (in ms) since epoch"""
|
||||||
# Refer to mtproto_plain_sender.py for the original method, this is a simple copy
|
# Refer to mtproto_plain_sender.py for the original method, this is a simple copy
|
||||||
new_msg_id = int(self.time_offset + time.time() * 1000)
|
new_msg_id = int(self.time_offset + time.time() * 1000)
|
||||||
if self._last_msg_id >= new_msg_id:
|
if self.last_message_id >= new_msg_id:
|
||||||
new_msg_id = self._last_msg_id + 4
|
new_msg_id = self._last_msg_id + 4
|
||||||
|
|
||||||
self._last_msg_id = new_msg_id
|
self._last_msg_id = new_msg_id
|
||||||
|
|
133
tl/telegram_client.py
Normal file
133
tl/telegram_client.py
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
# This file is based on TLSharp
|
||||||
|
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/TelegramClient.cs
|
||||||
|
from network.mtproto_sender import MtProtoSender
|
||||||
|
from network.tcp_transport import TcpTransport
|
||||||
|
import network.authenticator as authenticator
|
||||||
|
from tl.session import Session
|
||||||
|
import utils.helpers as utils
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
|
||||||
|
from tl.functions.invoke_with_layer import InvokeWithLayer
|
||||||
|
from tl.functions.init_connection import InitConnection
|
||||||
|
from tl.functions.help.get_config import GetConfig
|
||||||
|
from tl.functions.auth.check_phone import CheckPhone
|
||||||
|
from tl.functions.auth.send_code import SendCode
|
||||||
|
from tl.functions.auth.sign_in import SignIn
|
||||||
|
from tl.functions.contacts.get_contacts import GetContacts
|
||||||
|
from tl.types.input_peer_user import InputPeerUser
|
||||||
|
from tl.functions.messages.send_message import SendMessage
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramClient:
|
||||||
|
|
||||||
|
def __init__(self, session_user_id, layer, api_id=None, api_hash=None):
|
||||||
|
if api_id is None or api_hash is None:
|
||||||
|
raise PermissionError('Your API ID or Hash are invalid. Make sure to obtain yours in http://my.telegram.org')
|
||||||
|
|
||||||
|
self.api_id = api_id
|
||||||
|
self.api_hash = api_hash
|
||||||
|
|
||||||
|
self.layer = layer
|
||||||
|
|
||||||
|
self.session = Session.try_load_or_create_new(session_user_id)
|
||||||
|
self.transport = TcpTransport(self.session.server_address, self.session.port)
|
||||||
|
self.dc_options = None
|
||||||
|
|
||||||
|
# TODO Should this be async?
|
||||||
|
def connect(self, reconnect=False):
|
||||||
|
if self.session.auth_key is None or reconnect:
|
||||||
|
self.session.auth_key, self.session.time_offset= authenticator.do_authentication(self.transport)
|
||||||
|
|
||||||
|
self.sender = MtProtoSender(self.transport, self.session)
|
||||||
|
|
||||||
|
if not reconnect:
|
||||||
|
request = InvokeWithLayer(layer=self.layer,
|
||||||
|
query=InitConnection(api_id=self.api_id,
|
||||||
|
device_model=platform.node(),
|
||||||
|
system_version=platform.system(),
|
||||||
|
app_version='0.1',
|
||||||
|
lang_code='en',
|
||||||
|
query=GetConfig()))
|
||||||
|
|
||||||
|
self.sender.send(request)
|
||||||
|
self.sender.receive(request)
|
||||||
|
|
||||||
|
# Result is a Config TLObject
|
||||||
|
self.dc_options = request.result.dc_options
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def reconnect_to_dc(self, dc_id):
|
||||||
|
if self.dc_options is None or not self.dc_options:
|
||||||
|
raise ConnectionError("Can't reconnect. Stabilise an initial connection first.")
|
||||||
|
|
||||||
|
# dc is a DcOption TLObject
|
||||||
|
dc = next(dc for dc in self.dc_options if dc.id == dc_id)
|
||||||
|
|
||||||
|
self.transport = TcpTransport(dc.ip_address, dc.port)
|
||||||
|
self.session.server_address = dc.ip_address
|
||||||
|
self.session.port = dc.port
|
||||||
|
|
||||||
|
self.connect(reconnect=True)
|
||||||
|
|
||||||
|
def is_user_authorized(self):
|
||||||
|
return self.session.user is not None
|
||||||
|
|
||||||
|
def is_phone_registered(self, phone_number):
|
||||||
|
assert self.sender is not None, 'Not connected!'
|
||||||
|
|
||||||
|
request = CheckPhone(phone_number)
|
||||||
|
self.sender.send(request)
|
||||||
|
self.sender.receive(request)
|
||||||
|
|
||||||
|
# Result is an Auth.CheckedPhone
|
||||||
|
return request.result.phone_registered
|
||||||
|
|
||||||
|
def send_code_request(self, phone_number, destination='code'):
|
||||||
|
if destination == 'code':
|
||||||
|
destination = 5
|
||||||
|
elif destination == 'sms':
|
||||||
|
destination = 0
|
||||||
|
else:
|
||||||
|
raise ValueError('Destination must be either "code" or "sms"')
|
||||||
|
|
||||||
|
request = SendCode(phone_number, self.api_id, self.api_hash)
|
||||||
|
completed = False
|
||||||
|
while not completed:
|
||||||
|
try:
|
||||||
|
self.sender.send(request)
|
||||||
|
self.sender.receive(request)
|
||||||
|
completed = True
|
||||||
|
except ConnectionError as error:
|
||||||
|
if str(error).startswith('Your phone number is registered to'):
|
||||||
|
dc = int(re.search(r'\d+', str(error)).group(0))
|
||||||
|
self.reconnect_to_dc(dc)
|
||||||
|
else:
|
||||||
|
raise error
|
||||||
|
|
||||||
|
return request.result.phone_code_hash
|
||||||
|
|
||||||
|
def make_auth(self, phone_number, phone_code_hash, code):
|
||||||
|
request = SignIn(phone_number, phone_code_hash, code)
|
||||||
|
self.sender.send(request)
|
||||||
|
self.sender.receive(request)
|
||||||
|
|
||||||
|
# Result is an Auth.Authorization TLObject
|
||||||
|
self.session.user = request.result.user
|
||||||
|
self.session.save()
|
||||||
|
|
||||||
|
return self.session.user
|
||||||
|
|
||||||
|
def import_contacts(self, phone_code_hash):
|
||||||
|
request = GetContacts(phone_code_hash)
|
||||||
|
self.sender.send(request)
|
||||||
|
self.sender.receive(request)
|
||||||
|
return request.result.contacts, request.result.users
|
||||||
|
|
||||||
|
def send_message(self, user, message):
|
||||||
|
peer = InputPeerUser(user.id, user.access_hash)
|
||||||
|
request = SendMessage(peer, message, utils.generate_random_long())
|
||||||
|
|
||||||
|
self.sender.send(request)
|
||||||
|
self.sender.send(request)
|
|
@ -263,7 +263,7 @@ def write_onsend_code(builder, arg, args, name=None):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Else it may be a custom type
|
# Else it may be a custom type
|
||||||
builder.writeln('{}.write(writer)'.format(name))
|
builder.writeln('{}.on_send(writer)'.format(name))
|
||||||
|
|
||||||
# End vector and flag blocks if required (if we opened them before)
|
# End vector and flag blocks if required (if we opened them before)
|
||||||
if arg.is_vector:
|
if arg.is_vector:
|
||||||
|
@ -347,3 +347,12 @@ def write_onresponse_code(builder, arg, args, name=None):
|
||||||
|
|
||||||
if arg.is_flag:
|
if arg.is_flag:
|
||||||
builder.end_block()
|
builder.end_block()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if tlobjects_exist():
|
||||||
|
print('Detected previous TLObjects. Cleaning...')
|
||||||
|
clean_tlobjects()
|
||||||
|
|
||||||
|
print('Generating TLObjects...')
|
||||||
|
generate_tlobjects('scheme.tl')
|
||||||
|
print('Done.')
|
19
unit_test.py
19
unit_test.py
|
@ -1,15 +1,16 @@
|
||||||
import unittest
|
import random
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import random
|
import unittest
|
||||||
import utils.helpers as utils
|
|
||||||
|
|
||||||
|
import utils.helpers as utils
|
||||||
|
from crypto.aes import AES
|
||||||
|
from crypto.factorizator import Factorizator
|
||||||
|
from network.authenticator import do_authentication
|
||||||
from network.tcp_client import TcpClient
|
from network.tcp_client import TcpClient
|
||||||
|
from network.tcp_transport import TcpTransport
|
||||||
from utils.binary_reader import BinaryReader
|
from utils.binary_reader import BinaryReader
|
||||||
from utils.binary_writer import BinaryWriter
|
from utils.binary_writer import BinaryWriter
|
||||||
from utils.factorizator import Factorizator
|
|
||||||
from utils.aes import AES
|
|
||||||
|
|
||||||
|
|
||||||
host = 'localhost'
|
host = 'localhost'
|
||||||
port = random.randint(50000, 60000) # Arbitrary non-privileged port
|
port = random.randint(50000, 60000) # Arbitrary non-privileged port
|
||||||
|
@ -214,5 +215,11 @@ class UnitTest(unittest.TestCase):
|
||||||
assert cipher_text == real, 'Decrypted text does not equal the real value (expected "{}", got "{}")'\
|
assert cipher_text == real, 'Decrypted text does not equal the real value (expected "{}", got "{}")'\
|
||||||
.format(get_representation(real), get_representation(cipher_text))
|
.format(get_representation(real), get_representation(cipher_text))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_authenticator():
|
||||||
|
transport = TcpTransport('149.154.167.91', 443)
|
||||||
|
auth_key, time_offset = do_authentication(transport)
|
||||||
|
transport.dispose()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from io import BytesIO, BufferedReader
|
from io import BytesIO, BufferedReader
|
||||||
from tl.all_tlobjects import tlobjects
|
from tl.all_tlobjects import tlobjects
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,14 +86,20 @@ class BinaryReader:
|
||||||
|
|
||||||
def tgread_object(self):
|
def tgread_object(self):
|
||||||
"""Reads a Telegram object"""
|
"""Reads a Telegram object"""
|
||||||
id = self.read_int()
|
constructor_id = self.read_int()
|
||||||
clazz = tlobjects.get(id, None)
|
clazz = tlobjects.get(constructor_id, None)
|
||||||
if clazz is None:
|
if clazz is None:
|
||||||
raise ImportError('Could not find a matching ID for the TLObject that was supposed to be read. '
|
raise ImportError('Could not find a matching ID for the TLObject that was supposed to be read. '
|
||||||
'Found ID: {}'.format(hex(id)))
|
'Found ID: {}'.format(hex(constructor_id)))
|
||||||
|
|
||||||
# Instantiate the class and return the result
|
# Now we need to determine the number of parameters of the class, so we can
|
||||||
result = clazz()
|
# instantiate it with all of them set to None, and still, no need to write
|
||||||
|
# the default =None in all the classes, thus forcing the user to provide a real value
|
||||||
|
sig = inspect.signature(clazz.__init__)
|
||||||
|
params = [None] * (len(sig.parameters) - 1) # Subtract 1 (self)
|
||||||
|
result = clazz(*params) # https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists
|
||||||
|
|
||||||
|
# Finally, read the object and return the result
|
||||||
result.on_response(self)
|
result.on_response(self)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -100,7 +107,6 @@ class BinaryReader:
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.reader.close()
|
self.reader.close()
|
||||||
# TODO Do I need to close the underlying stream?
|
|
||||||
|
|
||||||
# region Position related
|
# region Position related
|
||||||
|
|
||||||
|
|
|
@ -65,18 +65,11 @@ class BinaryWriter:
|
||||||
if padding != 0:
|
if padding != 0:
|
||||||
padding = 4 - padding
|
padding = 4 - padding
|
||||||
|
|
||||||
# TODO ensure that _this_ is right (it appears to be)
|
|
||||||
self.write(bytes([254]))
|
self.write(bytes([254]))
|
||||||
self.write(bytes([len(data) % 256]))
|
self.write(bytes([len(data) % 256]))
|
||||||
self.write(bytes([(len(data) >> 8) % 256]))
|
self.write(bytes([(len(data) >> 8) % 256]))
|
||||||
self.write(bytes([(len(data) >> 16) % 256]))
|
self.write(bytes([(len(data) >> 16) % 256]))
|
||||||
self.write(data)
|
self.write(data)
|
||||||
""" Original:
|
|
||||||
binaryWriter.Write((byte)254);
|
|
||||||
binaryWriter.Write((byte)(bytes.Length));
|
|
||||||
binaryWriter.Write((byte)(bytes.Length >> 8));
|
|
||||||
binaryWriter.Write((byte)(bytes.Length >> 16));
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.write(bytes(padding))
|
self.write(bytes(padding))
|
||||||
|
|
||||||
|
@ -84,10 +77,10 @@ class BinaryWriter:
|
||||||
"""Write a string by using Telegram guidelines"""
|
"""Write a string by using Telegram guidelines"""
|
||||||
return self.tgwrite_bytes(string.encode('utf-8'))
|
return self.tgwrite_bytes(string.encode('utf-8'))
|
||||||
|
|
||||||
def tgwrite_bool(self, bool):
|
def tgwrite_bool(self, boolean):
|
||||||
"""Write a boolean value by using Telegram guidelines"""
|
"""Write a boolean value by using Telegram guidelines"""
|
||||||
# boolTrue boolFalse
|
# boolTrue boolFalse
|
||||||
return self.write_int(0x997275b5 if bool else 0xbc799737, signed=False)
|
return self.write_int(0x997275b5 if boolean else 0xbc799737, signed=False)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
@ -98,7 +91,6 @@ class BinaryWriter:
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the current stream"""
|
"""Close the current stream"""
|
||||||
self.writer.close()
|
self.writer.close()
|
||||||
# TODO Do I need to close the underlying stream?
|
|
||||||
|
|
||||||
def get_bytes(self, flush=True):
|
def get_bytes(self, flush=True):
|
||||||
"""Get the current bytes array content from the buffer, optionally flushing first"""
|
"""Get the current bytes array content from the buffer, optionally flushing first"""
|
||||||
|
|
|
@ -17,6 +17,7 @@ def generate_random_bytes(count):
|
||||||
|
|
||||||
|
|
||||||
def get_byte_array(integer, signed):
|
def get_byte_array(integer, signed):
|
||||||
|
"""Gets the arbitrary-length byte array corresponding to the given integer"""
|
||||||
bits = integer.bit_length()
|
bits = integer.bit_length()
|
||||||
byte_length = (bits + bits_per_byte - 1) // bits_per_byte
|
byte_length = (bits + bits_per_byte - 1) // bits_per_byte
|
||||||
# For some strange reason, this has to be big!
|
# For some strange reason, this has to be big!
|
||||||
|
@ -49,6 +50,7 @@ def calc_msg_key_offset(data, offset, limit):
|
||||||
|
|
||||||
|
|
||||||
def generate_key_data_from_nonces(server_nonce, new_nonce):
|
def generate_key_data_from_nonces(server_nonce, new_nonce):
|
||||||
|
"""Generates the key data corresponding to the given nonces"""
|
||||||
hash1 = sha1(bytes(new_nonce + server_nonce))
|
hash1 = sha1(bytes(new_nonce + server_nonce))
|
||||||
hash2 = sha1(bytes(server_nonce + new_nonce))
|
hash2 = sha1(bytes(server_nonce + new_nonce))
|
||||||
hash3 = sha1(bytes(new_nonce + new_nonce))
|
hash3 = sha1(bytes(new_nonce + new_nonce))
|
||||||
|
@ -66,6 +68,23 @@ def generate_key_data_from_nonces(server_nonce, new_nonce):
|
||||||
|
|
||||||
|
|
||||||
def sha1(data):
|
def sha1(data):
|
||||||
|
"""Calculates the SHA1 digest for the given data"""
|
||||||
sha = hashlib.sha1()
|
sha = hashlib.sha1()
|
||||||
sha.update(data)
|
sha.update(data)
|
||||||
return sha.digest()
|
return sha.digest()
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings():
|
||||||
|
"""Loads the user settings located under `api/`"""
|
||||||
|
settings = {}
|
||||||
|
with open('api/settings', 'r', encoding='utf-8') as file:
|
||||||
|
for line in file:
|
||||||
|
value_pair = line.split('=')
|
||||||
|
left = value_pair[0].strip()
|
||||||
|
right = value_pair[1].strip()
|
||||||
|
if right.isnumeric():
|
||||||
|
settings[left] = int(right)
|
||||||
|
else:
|
||||||
|
settings[left] = right
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
Loading…
Reference in New Issue
Block a user