Added and updated documentation

This commit is contained in:
Lonami 2016-08-28 13:43:00 +02:00
parent 5af1a4a5fc
commit bd1fee4048
16 changed files with 177 additions and 139 deletions

View File

@ -1,21 +1,27 @@
# Telethon # Telethon
**Telethon** is Telegram client implementation in Python. This project's _core_ is **completely based** on [TLSharp](https://github.com/sochix/TLSharp), so please, also have a look to the original project! **Telethon** is Telegram client implementation in Python. This project's _core_ is **completely based** on
[TLSharp](https://github.com/sochix/TLSharp). All the files which are fully based on it will have a notice
on the top of the file. Also don't forget to have a look to the original project.
Other parts, such as the request themselves, the .tl tokenizer and code generator, or some ported C# utilities such as `BinaryWriter`, `BinaryReader`, `TCPClient` and so on, are no longer part of TLSharp itself. The files without the previously mentioned notice are no longer part of TLSharp itself, or have enough modifications
to make them entirely different.
### Requirements ### Requirements
This project requires the following Python modules, which can be installed by issuing `sudo -H pip install <module>` on a Linux terminal: This project requires the following Python modules, which can be installed by issuing `sudo -H pip install <module>` on a
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! ### We need your help!
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 think it twice and help us (me)! 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
think it twice and help us (me)!
### 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.
Some parts of the .tl file _should_ be omitted, because they're "built-in" in the generated code (such as writing booleans, etc.). Some parts of the `.tl` file _should_ be omitted, because they're "built-in" in the generated code (such as writing booleans, etc.).
In order to make sure that all the generated files will work, please make sure to **always** comment out these lines in `scheme.tl` In order to make sure that all the generated files will work, please make sure to **always** comment out these lines in `scheme.tl`
(the latest version can always be found [here](https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/scheme.tl)): (the latest version can always be found
[here](https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/scheme.tl)):
```tl ```tl
// boolFalse#bc799737 = Bool; // boolFalse#bc799737 = Bool;
@ -24,6 +30,5 @@ In order to make sure that all the generated files will work, please make sure t
// vector#1cb5c415 {t:Type} # [ t ] = Vector t; // vector#1cb5c415 {t:Type} # [ t ] = Vector t;
``` ```
Also please make sure to rename `updates#74ae4240 ...` to `updates_tg#74ae4240 ...` or similar to avoid confusion between the updates folder and the updates.py file! Also please make sure to rename `updates#74ae4240 ...` to `updates_tg#74ae4240 ...` or similar to avoid confusion between
the `updates` folder and the `updates.py` file!

12
main.py
View File

@ -1,7 +1,9 @@
import tl.generator import parser.tl_generator
if __name__ == '__main__': if __name__ == '__main__':
if not tl.generator.tlobjects_exist(): if not parser.tl_generator.tlobjects_exist():
print('Please run tl/generator.py at least once before continuing') print('First run. Generating TLObjects...')
else: parser.tl_generator.generate_tlobjects('scheme.tl')
pass print('Done.')
pass

View File

@ -1,11 +1,12 @@
# This file is based on TLSharp
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/MtProtoPlainSender.cs
import time import time
from utils.binary_writer import BinaryWriter from utils.binary_writer import BinaryWriter
from utils.binary_reader import BinaryReader from utils.binary_reader import BinaryReader
class MtProtoPlainSender: class MtProtoPlainSender:
"""MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)"""
def __init__(self, transport): def __init__(self, transport):
self._sequence = 0 self._sequence = 0
self._time_offset = 0 self._time_offset = 0
@ -13,6 +14,7 @@ class MtProtoPlainSender:
self._transport = transport self._transport = transport
def send(self, data): def send(self, data):
"""Sends a plain packet (auth_key_id = 0) containing the given message body (data)"""
with BinaryWriter() as writer: with BinaryWriter() as writer:
writer.write_long(0) writer.write_long(0)
writer.write_int(self.get_new_msg_id()) writer.write_int(self.get_new_msg_id())
@ -23,18 +25,21 @@ class MtProtoPlainSender:
self._transport.send(packet) self._transport.send(packet)
def receive(self): def receive(self):
"""Receives a plain packet, returning the body of the response"""
result = self._transport.receive() result = self._transport.receive()
with BinaryReader(result.body) as reader: with BinaryReader(result.body) as reader:
auth_key_id = reader.read_long() auth_key_id = reader.read_long()
message_id = reader.read_long() msg_id = reader.read_long()
message_length = reader.read_int() message_length = reader.read_int()
response = reader.read(message_length) response = reader.read(message_length)
return response return response
def get_new_msg_id(self): def get_new_msg_id(self):
new_msg_id = int(self._time_offset + time.time() * 1000) # multiply by 1000 to get milliseconds """Generates a new message ID based on the current time (in ms) since epoch"""
new_msg_id = int(self._time_offset + time.time() * 1000) # Multiply by 1000 to get milliseconds
# Ensure that we always return a message ID which is higher than the previous one
if self._last_msg_id >= new_msg_id: if self._last_msg_id >= new_msg_id:
new_msg_id = self._last_msg_id + 4 new_msg_id = self._last_msg_id + 4

View File

@ -1,3 +1,5 @@
# This file is based on TLSharp
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/MtProtoSender.cs
import re import re
import zlib import zlib
import pyaes import pyaes
@ -5,51 +7,68 @@ from time import sleep
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 requests.ack_request import AckRequest from tl.types.msgs_ack import MsgsAck
import utils.helpers as helpers import utils.helpers as helpers
class MtProtoSender: class MtProtoSender:
"""MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)"""
def __init__(self, transport, session): def __init__(self, transport, session):
self._transport = transport self.transport = transport
self._session = session self.session = session
self.need_confirmation = [] # Message IDs that need confirmation
self.need_confirmation = []
def change_transport(self, transport):
self._transport = transport
def generate_sequence(self, confirmed): def generate_sequence(self, confirmed):
"""Generates the next sequence number, based on whether it was confirmed yet or not"""
if confirmed: if confirmed:
result = self._session.sequence * 2 + 1 result = self.session.sequence * 2 + 1
self._session.sequence += 1 self.session.sequence += 1
return result return result
else: else:
return self._session.sequence * 2 return self.session.sequence * 2
# TODO async? # region Send and receive
# TODO In TLSharp, this was async. Should this be?
def send(self, request): def send(self, request):
if self.need_confirmation: """Sends the specified MTProtoRequest, previously sending any message which needed confirmation"""
ack_request = AckRequest(self.need_confirmation)
# First check if any message needs confirmation, if this is the case, send an "AckRequest"
if self.need_confirmation:
msgs_ack = MsgsAck(self.need_confirmation)
with BinaryWriter() as writer: with BinaryWriter() as writer:
ack_request.on_send(writer) msgs_ack.on_send(writer)
self.send_packet(writer.get_bytes(), ack_request) self.send_packet(writer.get_bytes(), msgs_ack)
del self.need_confirmation[:] del self.need_confirmation[:]
# Then send our packed request
with BinaryWriter() as writer: with BinaryWriter() as writer:
request.on_send(writer) request.on_send(writer)
self.send_packet(writer.get_bytes(), request) self.send_packet(writer.get_bytes(), request)
self._session.save() # And update the saved session
self.session.save()
def receive(self, request):
"""Receives the specified MTProtoRequest ("fills in it" the received data)"""
while not request.confirm_received:
message, remote_msg_id, remote_sequence = self.decode_msg(self.transport.receive().body)
with BinaryReader(message) as reader:
self.process_msg(remote_msg_id, remote_sequence, reader, request)
# endregion
# region Low level processing
def send_packet(self, packet, request): def send_packet(self, packet, request):
request.message_id = self._session.get_new_msg_id() """Sends the given packet bytes with the additional information of the original request"""
request.msg_id = self.session.get_new_msg_id()
# First calculate the ciphered bit
with BinaryWriter() as writer: with BinaryWriter() as writer:
# TODO Is there any difference with unsigned long and long? writer.write_long(self.session.salt, signed=False)
writer.write_long(self._session.salt, signed=False) writer.write_long(self.session.id, signed=False)
writer.write_long(self._session.id, signed=False)
writer.write_long(request.msg_id) writer.write_long(request.msg_id)
writer.write_int(self.generate_sequence(request.confirmed)) writer.write_int(self.generate_sequence(request.confirmed))
writer.write_int(len(packet)) writer.write_int(len(packet))
@ -57,21 +76,22 @@ 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.data, msg_key, True)
aes = pyaes.AESModeOfOperationCFB(key, iv, 16) aes = pyaes.AESModeOfOperationCFB(key, iv, 16)
cipher_text = aes.encrypt(writer.get_bytes()) cipher_text = aes.encrypt(writer.get_bytes())
# And then finally send the packet
with BinaryWriter() as writer: with BinaryWriter() as writer:
# TODO is it unsigned long? writer.write_long(self.session.auth_key.id, signed=False)
writer.write_long(self._session.auth_key.id, signed=False)
writer.write(msg_key) writer.write(msg_key)
writer.write(cipher_text) writer.write(cipher_text)
self._transport.send(writer.get_bytes()) self.transport.send(writer.get_bytes())
def decode_msg(self, body): def decode_msg(self, body):
"""Decodes an received encrypted message body bytes"""
message = None message = None
remote_message_id = None remote_msg_id = None
remote_sequence = None remote_sequence = None
with BinaryReader(body) as reader: with BinaryReader(body) as reader:
@ -82,72 +102,70 @@ class MtProtoSender:
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) aes = pyaes.AESModeOfOperationCFB(key, iv, 16)
plain_text = aes.decrypt(reader.read(len(body) - reader.tell_position())) 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()
remote_session_id = plain_text_reader.read_long() remote_session_id = plain_text_reader.read_long()
remote_message_id = plain_text_reader.read_long() remote_msg_id = plain_text_reader.read_long()
remote_sequence = plain_text_reader.read_int() remote_sequence = plain_text_reader.read_int()
msg_len = plain_text_reader.read_int() msg_len = plain_text_reader.read_int()
message = plain_text_reader.read(msg_len) message = plain_text_reader.read(msg_len)
return message, remote_message_id, remote_sequence return message, remote_msg_id, remote_sequence
def receive(self, mtproto_request): def process_msg(self, msg_id, sequence, reader, request):
while not mtproto_request.confirm_received: """Processes and handles a Telegram message"""
message, remote_message_id, remote_sequence = self.decode_msg(self._transport.receive().body)
with BinaryReader(message) as reader:
self.process_msg(remote_message_id, remote_sequence, reader, mtproto_request)
def process_msg(self, message_id, sequence, reader, mtproto_request):
# TODO Check salt, session_id and sequence_number # TODO Check salt, session_id and sequence_number
self.need_confirmation.append(message_id) self.need_confirmation.append(msg_id)
code = reader.read_int(signed=False) code = reader.read_int(signed=False)
reader.seek(-4) reader.seek(-4)
if code == 0x73f1f8dc: # Container if code == 0x73f1f8dc: # Container
return self.handle_container(message_id, sequence, reader, mtproto_request) return self.handle_container(msg_id, sequence, reader, request)
if code == 0x7abe77ec: # Ping if code == 0x7abe77ec: # Ping
return self.handle_ping(message_id, sequence, reader) return self.handle_ping(msg_id, sequence, reader)
if code == 0x347773c5: # pong if code == 0x347773c5: # pong
return self.handle_pong(message_id, sequence, reader) return self.handle_pong(msg_id, sequence, reader)
if code == 0xae500895: # future_salts if code == 0xae500895: # future_salts
return self.handle_future_salts(message_id, sequence, reader) return self.handle_future_salts(msg_id, sequence, reader)
if code == 0x9ec20908: # new_session_created if code == 0x9ec20908: # new_session_created
return self.handle_new_session_created(message_id, sequence, reader) return self.handle_new_session_created(msg_id, sequence, reader)
if code == 0x62d6b459: # msgs_ack if code == 0x62d6b459: # msgs_ack
return self.handle_msgs_ack(message_id, sequence, reader) return self.handle_msgs_ack(msg_id, sequence, reader)
if code == 0xedab447b: # bad_server_salt if code == 0xedab447b: # bad_server_salt
return self.handle_bad_server_salt(message_id, sequence, reader, mtproto_request) return self.handle_bad_server_salt(msg_id, sequence, reader, request)
if code == 0xa7eff811: # bad_msg_notification if code == 0xa7eff811: # bad_msg_notification
return self.handle_bad_msg_notification(message_id, sequence, reader) return self.handle_bad_msg_notification(msg_id, sequence, reader)
if code == 0x276d3ec6: # msg_detailed_info if code == 0x276d3ec6: # msg_detailed_info
return self.hangle_msg_detailed_info(message_id, sequence, reader) return self.hangle_msg_detailed_info(msg_id, sequence, reader)
if code == 0xf35c6d01: # rpc_result if code == 0xf35c6d01: # rpc_result
return self.handle_rpc_result(message_id, sequence, reader, mtproto_request) return self.handle_rpc_result(msg_id, sequence, reader, request)
if code == 0x3072cfa1: # gzip_packed if code == 0x3072cfa1: # gzip_packed
return self.handle_gzip_packed(message_id, sequence, reader, mtproto_request) return self.handle_gzip_packed(msg_id, sequence, reader, request)
if (code == 0xe317af7e or if (code == 0xe317af7e or
code == 0xd3f45784 or code == 0xd3f45784 or
code == 0x2b2fbd4e or code == 0x2b2fbd4e or
code == 0x78d4dec1 or code == 0x78d4dec1 or
code == 0x725b04c3 or code == 0x725b04c3 or
code == 0x74ae4240): code == 0x74ae4240):
return self.handle_update(message_id, sequence, reader) return self.handle_update(msg_id, sequence, reader)
# TODO Log unknown message code print('Unknown message: {}'.format(hex(msg_id)))
return False return False
def handle_update(self, message_id, sequence, reader): # endregion
# region Message handling
def handle_update(self, msg_id, sequence, reader):
return False return False
def handle_container(self, message_id, sequence, reader, mtproto_request): def handle_container(self, msg_id, sequence, reader, request):
code = reader.read_int(signed=False) code = reader.read_int(signed=False)
size = reader.read_int() size = reader.read_int()
for _ in range(size): for _ in range(size):
@ -156,7 +174,7 @@ class MtProtoSender:
inner_length = reader.read_int() inner_length = reader.read_int()
begin_position = reader.tell_position() begin_position = reader.tell_position()
try: try:
if not self.process_msg(inner_msg_id, sequence, reader, mtproto_request): if not self.process_msg(inner_msg_id, sequence, reader, request):
reader.set_position(begin_position + inner_length) reader.set_position(begin_position + inner_length)
except: except:
@ -164,40 +182,40 @@ class MtProtoSender:
return False return False
def handle_ping(self, message_id, sequence, reader): def handle_ping(self, msg_id, sequence, reader):
return False return False
def handle_pong(self, message_id, sequence, reader): def handle_pong(self, msg_id, sequence, reader):
return False return False
def handle_future_salts(self, message_id, sequence, reader): def handle_future_salts(self, msg_id, sequence, reader):
code = reader.read_int(signed=False) code = reader.read_int(signed=False)
request_id = reader.read_long(signed=False) request_id = reader.read_long(signed=False)
reader.seek(-12) reader.seek(-12)
raise NotImplementedError("Handle future server salts function isn't implemented.") raise NotImplementedError("Handle future server salts function isn't implemented.")
def handle_new_session_created(self, message_id, sequence, reader): def handle_new_session_created(self, msg_id, sequence, reader):
return False return False
def handle_msgs_ack(self, message_id, sequence, reader): def handle_msgs_ack(self, msg_id, sequence, reader):
return False return False
def handle_bad_server_salt(self, message_id, sequence, reader, mtproto_request): def handle_bad_server_salt(self, msg_id, sequence, reader, mtproto_request):
code = reader.read_int(signed=False) code = reader.read_int(signed=False)
bad_msg_id = reader.read_long(signed=False) bad_msg_id = reader.read_long(signed=False)
bad_msg_seq_no = reader.read_int() bad_msg_seq_no = reader.read_int()
error_code = reader.read_int() error_code = reader.read_int()
new_salt = reader.read_long(signed=False) new_salt = reader.read_long(signed=False)
self._session.salt = new_salt self.session.salt = new_salt
# Resend # Resend
self.send(mtproto_request) self.send(mtproto_request)
return True return True
def handle_bad_msg_notification(self, message_id, sequence, reader): def handle_bad_msg_notification(self, msg_id, sequence, reader):
code = reader.read_int(signed=False) code = reader.read_int(signed=False)
request_id = reader.read_long(signed=False) request_id = reader.read_long(signed=False)
request_sequence = reader.read_int() request_sequence = reader.read_int()
@ -238,14 +256,14 @@ class MtProtoSender:
raise NotImplementedError('This should never happen!') raise NotImplementedError('This should never happen!')
def hangle_msg_detailed_info(self, message_id, sequence, reader): def hangle_msg_detailed_info(self, msg_id, sequence, reader):
return False return False
def handle_rpc_result(self, message_id, sequence, reader, mtproto_request): def handle_rpc_result(self, msg_id, sequence, reader, mtproto_request):
code = reader.read_int(signed=False) code = reader.read_int(signed=False)
request_id = reader.read_long(signed=False) request_id = reader.read_long(signed=False)
if request_id == mtproto_request.message_id: if request_id == mtproto_request.msg_id:
mtproto_request.confirm_received = True mtproto_request.confirm_received = True
inner_code = reader.read_int(signed=False) inner_code = reader.read_int(signed=False)
@ -273,7 +291,6 @@ class MtProtoSender:
with BinaryReader(unpacked_data) as compressed_reader: with BinaryReader(unpacked_data) as compressed_reader:
mtproto_request.on_response(compressed_reader) mtproto_request.on_response(compressed_reader)
except: except:
pass pass
@ -281,10 +298,13 @@ class MtProtoSender:
reader.seek(-4) reader.seek(-4)
mtproto_request.on_response(reader) mtproto_request.on_response(reader)
def handle_gzip_packed(self, message_id, sequence, reader, mtproto_request): def handle_gzip_packed(self, msg_id, sequence, reader, mtproto_request):
code = reader.read_int(signed=False) code = reader.read_int(signed=False)
packed_data = reader.tgread_bytes() packed_data = reader.tgread_bytes()
unpacked_data = zlib.decompress(packed_data) unpacked_data = zlib.decompress(packed_data)
with BinaryReader(unpacked_data) as compressed_reader: with BinaryReader(unpacked_data) as compressed_reader:
self.process_msg(message_id, sequence, compressed_reader, mtproto_request) self.process_msg(msg_id, sequence, compressed_reader, mtproto_request)
# endregion
pass

View File

@ -1,20 +1,24 @@
# Python rough implementation of a C# TCP client
import socket import socket
class TcpClient: class TcpClient:
def __init__(self): def __init__(self):
self.connected = False self.connected = False
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
def connect(self, ip, port): def connect(self, ip, port):
"""Connects to the specified IP and port number"""
self.socket.connect((ip, port)) self.socket.connect((ip, port))
def close(self): def close(self):
"""Closes the connection"""
self.socket.close() self.socket.close()
def write(self, data): def write(self, data):
"""Writes (sends) the specified bytes to the connected peer"""
self.socket.send(data) self.socket.send(data)
def read(self, buffer_size): def read(self, buffer_size):
"""Reads (receives) the specified bytes from the connected peer"""
self.socket.recv(buffer_size) self.socket.recv(buffer_size)

View File

@ -1,4 +1,5 @@
# This file is based on TLSharp
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/TcpMessage.cs
from zlib import crc32 from zlib import crc32
from utils.binary_writer import BinaryWriter from utils.binary_writer import BinaryWriter
@ -6,7 +7,6 @@ from utils.binary_reader import BinaryReader
class TcpMessage: class TcpMessage:
def __init__(self, seq_number, body): def __init__(self, seq_number, body):
""" """
:param seq_number: Sequence number :param seq_number: Sequence number
@ -19,6 +19,7 @@ class TcpMessage:
self.body = body self.body = body
def encode(self): def encode(self):
"""Returns the bytes of the this message encoded, following Telegram's guidelines"""
with BinaryWriter() as writer: with BinaryWriter() as writer:
''' https://core.telegram.org/mtproto#tcp-transport ''' https://core.telegram.org/mtproto#tcp-transport
@ -38,7 +39,9 @@ class TcpMessage:
return writer.get_bytes() return writer.get_bytes()
def decode(self, body): @staticmethod
def decode(body):
"""Returns a TcpMessage from the given encoded bytes, decoding them previously"""
if body is None: if body is None:
raise ValueError('body cannot be None') raise ValueError('body cannot be None')

View File

@ -1,11 +1,11 @@
# This file is based on TLSharp
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/TcpTransport.cs
from zlib import crc32 from zlib import crc32
from network.tcp_message import TcpMessage from network.tcp_message import TcpMessage
from network.tcp_client import TcpClient from network.tcp_client import TcpClient
class TcpTransport: class TcpTransport:
def __init__(self, ip_address, port): def __init__(self, ip_address, port):
self._tcp_client = TcpClient() self._tcp_client = TcpClient()
self._send_counter = 0 self._send_counter = 0
@ -13,20 +13,20 @@ class TcpTransport:
self._tcp_client.connect(ip_address, port) self._tcp_client.connect(ip_address, port)
def send(self, packet): def send(self, packet):
""" """Sends the given packet (bytes array) to the connected peer"""
:param packet: Bytes array representing the packet to be sent
"""
if not self._tcp_client.connected: if not self._tcp_client.connected:
raise ConnectionError('Client not connected to server.') raise ConnectionError('Client not connected to server.')
# Get a TcpMessage which contains the given packet
tcp_message = TcpMessage(self._send_counter, packet) tcp_message = TcpMessage(self._send_counter, packet)
# TODO async? and receive too, of course # TODO In TLSharp, this is async; Should both send and receive be here too?
self._tcp_client.write(tcp_message.encode()) self._tcp_client.write(tcp_message.encode())
self._send_counter += 1 self._send_counter += 1
def receive(self): def receive(self):
"""Receives a TcpMessage from the connected peer"""
# First read everything # First read everything
packet_length_bytes = self._tcp_client.read(4) packet_length_bytes = self._tcp_client.read(4)
packet_length = int.from_bytes(packet_length_bytes, byteorder='big') packet_length = int.from_bytes(packet_length_bytes, byteorder='big')
@ -45,6 +45,7 @@ class TcpTransport:
if checksum != valid_checksum: if checksum != valid_checksum:
raise ValueError('Invalid checksum, skip') raise ValueError('Invalid checksum, skip')
# If we passed the tests, we can then return a valid TcpMessage
return TcpMessage(seq, body) return TcpMessage(seq, body)
def dispose(self): def dispose(self):

View File

@ -1,23 +1,18 @@
from io import StringIO
class SourceBuilder: class SourceBuilder:
"""This class should be used to build .py source files""" """This class should be used to build .py source files"""
def __init__(self, out_stream=None, indent_size=4): def __init__(self, out_stream, indent_size=4):
self.current_indent = 0 self.current_indent = 0
self.on_new_line = False self.on_new_line = False
self.indent_size = indent_size self.indent_size = indent_size
self.out_stream = out_stream
if out_stream is None:
self.out_stream = StringIO()
else:
self.out_stream = out_stream
def indent(self): def indent(self):
"""Indents the current source code line by the current indentation level"""
self.write(' ' * (self.current_indent * self.indent_size)) self.write(' ' * (self.current_indent * self.indent_size))
def write(self, string): def write(self, string):
"""Writes a string into the source code, applying indentation if required"""
if self.on_new_line: if self.on_new_line:
self.on_new_line = False # We're not on a new line anymore self.on_new_line = False # We're not on a new line anymore
if string.strip(): # If the string was not empty, indent; Else it probably was a new line if string.strip(): # If the string was not empty, indent; Else it probably was a new line
@ -26,6 +21,7 @@ class SourceBuilder:
self.out_stream.write(string) self.out_stream.write(string)
def writeln(self, string=''): def writeln(self, string=''):
"""Writes a string into the source code _and_ appends a new line, applying indentation if required"""
self.write(string + '\n') self.write(string + '\n')
self.on_new_line = True self.on_new_line = True
@ -34,6 +30,7 @@ class SourceBuilder:
self.current_indent += 1 self.current_indent += 1
def end_block(self): def end_block(self):
"""Ends an indentation block, leaving an empty line afterwards"""
self.current_indent -= 1 self.current_indent -= 1
self.writeln() self.writeln()

View File

@ -1,6 +1,7 @@
import os import os
import re import re
import shutil import shutil
from parser.tl_parser import TLParser from parser.tl_parser import TLParser
from parser.source_builder import SourceBuilder from parser.source_builder import SourceBuilder
@ -52,7 +53,7 @@ def generate_tlobjects(scheme_file):
with open(filename, 'w', encoding='utf-8') as file: with open(filename, 'w', encoding='utf-8') as file:
# Let's build the source code! # Let's build the source code!
with SourceBuilder(file) as builder: with SourceBuilder(file) as builder:
builder.writeln('from requests.mtproto_request import MTProtoRequest') builder.writeln('from tl.mtproto_request import MTProtoRequest')
builder.writeln() builder.writeln()
builder.writeln() builder.writeln()
builder.writeln('class {}(MTProtoRequest):'.format(get_class_name(tlobject))) builder.writeln('class {}(MTProtoRequest):'.format(get_class_name(tlobject)))
@ -346,8 +347,3 @@ 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__':
clean_tlobjects()
generate_tlobjects('scheme.tl')

View File

View File

@ -1,20 +0,0 @@
from requests.mtproto_request import MTProtoRequest
class AckRequest(MTProtoRequest):
def __init__(self, msgs):
super().__init__()
self.msgs = msgs
def on_send(self, writer):
writer.write_int(0x62d6b459) # msgs_ack
writer.write_int(0x1cb5c415) # vector
writer.write_int(len(self.msgs))
for msg_id in self.msgs:
writer.write_int(msg_id, signed=False)
def on_response(self, reader):
pass
def on_exception(self, exception):
pass

View File

@ -1,3 +1,5 @@
# This file is based on TLSharp
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Requests/MTProtoRequest.cs
from datetime import datetime, timedelta from datetime import datetime, timedelta

View File

@ -21,18 +21,23 @@ class BinaryReader:
# region Reading # region Reading
def read_int(self, signed=True): def read_int(self, signed=True):
"""Reads an integer (4 bytes) value"""
return int.from_bytes(self.reader.read(4), signed=signed, byteorder='big') return int.from_bytes(self.reader.read(4), signed=signed, byteorder='big')
def read_long(self, signed=True): def read_long(self, signed=True):
"""Reads a long integer (8 bytes) value"""
return int.from_bytes(self.reader.read(8), signed=signed, byteorder='big') return int.from_bytes(self.reader.read(8), signed=signed, byteorder='big')
def read_large_int(self, bits): def read_large_int(self, bits):
"""Reads a n-bits long integer value"""
return int.from_bytes(self.reader.read(bits // 8), byteorder='big') return int.from_bytes(self.reader.read(bits // 8), byteorder='big')
def read(self, length): def read(self, length):
"""Read the given amount of bytes"""
return self.reader.read(length) return self.reader.read(length)
def get_bytes(self): def get_bytes(self):
"""Gets the byte array representing the current buffer as a whole"""
return self.stream.getbuffer() return self.stream.getbuffer()
# endregion # endregion
@ -40,6 +45,7 @@ class BinaryReader:
# region Telegram custom reading # region Telegram custom reading
def tgread_bytes(self): def tgread_bytes(self):
"""Reads a Telegram-encoded byte array, without the need of specifying its length"""
first_byte = self.read(1) first_byte = self.read(1)
if first_byte == 254: if first_byte == 254:
length = self.read(1) | (self.read(1) << 8) | (self.read(1) << 16) length = self.read(1) | (self.read(1) << 8) | (self.read(1) << 16)
@ -56,6 +62,7 @@ class BinaryReader:
return data return data
def tgread_string(self): def tgread_string(self):
"""Reads a Telegram-encoded string"""
return str(self.tgread_bytes(), encoding='utf-8') return str(self.tgread_bytes(), encoding='utf-8')
def tgread_object(self): def tgread_object(self):

View File

@ -18,9 +18,11 @@ class BinaryWriter:
# region Writing # region Writing
def write_byte(self, value): def write_byte(self, value):
"""Writes a single byte value"""
self.writer.write(pack('B', value)) self.writer.write(pack('B', value))
def write_int(self, value, signed=True): def write_int(self, value, signed=True):
"""Writes an integer value (4 bytes), which can or cannot be signed"""
if signed: if signed:
self.writer.write(pack('i', value)) self.writer.write(pack('i', value))
else: else:
@ -28,6 +30,7 @@ class BinaryWriter:
self.writer.write(pack('I', value)) self.writer.write(pack('I', value))
def write_long(self, value, signed=True): def write_long(self, value, signed=True):
"""Writes a long integer value (8 bytes), which can or cannot be signed"""
if signed: if signed:
self.writer.write(pack('q', value)) self.writer.write(pack('q', value))
else: else:
@ -35,15 +38,19 @@ class BinaryWriter:
self.writer.write(pack('Q', value)) self.writer.write(pack('Q', value))
def write_float(self, value): def write_float(self, value):
"""Writes a floating point value (4 bytes)"""
self.writer.write(pack('f', value)) self.writer.write(pack('f', value))
def write_double(self, value): def write_double(self, value):
"""Writes a floating point value (8 bytes)"""
self.writer.write(pack('d', value)) self.writer.write(pack('d', value))
def write_large_int(self, value, bits): def write_large_int(self, value, bits):
"""Writes a n-bits long integer value"""
self.writer.write(pack('{}B'.format(bits // 8), value)) self.writer.write(pack('{}B'.format(bits // 8), value))
def write(self, data): def write(self, data):
"""Writes the given bytes array"""
self.writer.write(data) self.writer.write(data)
# endregion # endregion
@ -51,7 +58,7 @@ class BinaryWriter:
# region Telegram custom writing # region Telegram custom writing
def tgwrite_bytes(self, data): def tgwrite_bytes(self, data):
"""Write bytes by using Telegram guidelines"""
if len(data) < 254: if len(data) < 254:
padding = (len(data) + 1) % 4 padding = (len(data) + 1) % 4
if padding != 0: if padding != 0:
@ -71,7 +78,6 @@ class BinaryWriter:
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: """ Original:
binaryWriter.Write((byte)254); binaryWriter.Write((byte)254);
binaryWriter.Write((byte)(bytes.Length)); binaryWriter.Write((byte)(bytes.Length));
@ -82,22 +88,27 @@ class BinaryWriter:
self.write(bytes(padding)) self.write(bytes(padding))
def tgwrite_string(self, string): def tgwrite_string(self, string):
"""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, bool):
"""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 bool else 0xbc799737, signed=False)
# endregion # endregion
def flush(self): def flush(self):
"""Flush the current stream to "update" changes"""
self.writer.flush() self.writer.flush()
def close(self): def close(self):
"""Close the current stream"""
self.writer.close() self.writer.close()
# TODO Do I need to close the underlying stream? # 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"""
if flush: if flush:
self.writer.flush() self.writer.flush()
self.stream.getbuffer() self.stream.getbuffer()

View File

@ -4,6 +4,7 @@ from hashlib import sha1
def generate_random_long(signed=True): def generate_random_long(signed=True):
"""Generates a random long integer (8 bytes), which is optionally signed"""
result = random.getrandbits(64) result = random.getrandbits(64)
if not signed: if not signed:
result &= 0xFFFFFFFFFFFFFFFF # Ensure it's unsigned result &= 0xFFFFFFFFFFFFFFFF # Ensure it's unsigned
@ -12,6 +13,7 @@ def generate_random_long(signed=True):
def generate_random_bytes(count): def generate_random_bytes(count):
"""Generates a random bytes array"""
with BinaryWriter() as writer: with BinaryWriter() as writer:
for _ in range(count): for _ in range(count):
writer.write(random.getrandbits(8)) writer.write(random.getrandbits(8))
@ -20,6 +22,7 @@ def generate_random_bytes(count):
def calc_key(shared_key, msg_key, client): def calc_key(shared_key, msg_key, client):
"""Calculate the key based on Telegram guidelines, specifying whether it's the client or not"""
x = 0 if client else 8 x = 0 if client else 8
buffer = [0] * 48 buffer = [0] * 48
@ -47,10 +50,12 @@ def calc_key(shared_key, msg_key, client):
def calc_msg_key(data): def calc_msg_key(data):
"""Calculates the message key from the given data"""
return sha1(data)[4:20] return sha1(data)[4:20]
def calc_msg_key_offset(data, offset, limit): def calc_msg_key_offset(data, offset, limit):
"""Calculates the message key from offset given data, with an optional offset and limit"""
# TODO untested, may not be offset like this # TODO untested, may not be offset like this
# In the original code it was as parameters for the sha function, not slicing the array # In the original code it was as parameters for the sha function, not slicing the array
return sha1(data[offset:offset + limit])[4:20] return sha1(data[offset:offset + limit])[4:20]