diff --git a/README.md b/README.md index b7106d20..e73961e1 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,27 @@ # 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 -This project requires the following Python modules, which can be installed by issuing `sudo -H pip install ` on a Linux terminal: +This project requires the following Python modules, which can be installed by issuing `sudo -H pip install ` on a +Linux terminal: - `pyaes` ([GitHub](https://github.com/ricmoo/pyaes), [package index](https://pypi.python.org/pypi/pyaes)) ### 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 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` -(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 // 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; ``` -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! - - \ No newline at end of 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! diff --git a/main.py b/main.py index 32441c6c..b4a3863b 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,9 @@ -import tl.generator +import parser.tl_generator if __name__ == '__main__': - if not tl.generator.tlobjects_exist(): - print('Please run tl/generator.py at least once before continuing') - else: - pass + if not parser.tl_generator.tlobjects_exist(): + print('First run. Generating TLObjects...') + parser.tl_generator.generate_tlobjects('scheme.tl') + print('Done.') + + pass diff --git a/network/mtproto_plain_sender.py b/network/mtproto_plain_sender.py index a2e81398..80d0cf1c 100644 --- a/network/mtproto_plain_sender.py +++ b/network/mtproto_plain_sender.py @@ -1,11 +1,12 @@ - +# This file is based on TLSharp +# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/MtProtoPlainSender.cs import time from utils.binary_writer import BinaryWriter from utils.binary_reader import BinaryReader class MtProtoPlainSender: - + """MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)""" def __init__(self, transport): self._sequence = 0 self._time_offset = 0 @@ -13,6 +14,7 @@ class MtProtoPlainSender: self._transport = transport def send(self, data): + """Sends a plain packet (auth_key_id = 0) containing the given message body (data)""" with BinaryWriter() as writer: writer.write_long(0) writer.write_int(self.get_new_msg_id()) @@ -23,18 +25,21 @@ class MtProtoPlainSender: self._transport.send(packet) def receive(self): + """Receives a plain packet, returning the body of the response""" result = self._transport.receive() with BinaryReader(result.body) as reader: auth_key_id = reader.read_long() - message_id = reader.read_long() + msg_id = reader.read_long() message_length = reader.read_int() response = reader.read(message_length) return response 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: new_msg_id = self._last_msg_id + 4 diff --git a/network/mtproto_sender.py b/network/mtproto_sender.py index b43add5c..ea328072 100644 --- a/network/mtproto_sender.py +++ b/network/mtproto_sender.py @@ -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 zlib import pyaes @@ -5,51 +7,68 @@ from time import sleep from utils.binary_writer import BinaryWriter from utils.binary_reader import BinaryReader -from requests.ack_request import AckRequest +from tl.types.msgs_ack import MsgsAck import utils.helpers as helpers class MtProtoSender: + """MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)""" def __init__(self, transport, session): - self._transport = transport - self._session = session - - self.need_confirmation = [] - - def change_transport(self, transport): - self._transport = transport + self.transport = transport + self.session = session + self.need_confirmation = [] # Message IDs that need confirmation def generate_sequence(self, confirmed): + """Generates the next sequence number, based on whether it was confirmed yet or not""" if confirmed: - result = self._session.sequence * 2 + 1 - self._session.sequence += 1 + result = self.session.sequence * 2 + 1 + self.session.sequence += 1 return result 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): - if self.need_confirmation: - ack_request = AckRequest(self.need_confirmation) + """Sends the specified MTProtoRequest, previously sending any message which needed 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: - ack_request.on_send(writer) - self.send_packet(writer.get_bytes(), ack_request) + msgs_ack.on_send(writer) + self.send_packet(writer.get_bytes(), msgs_ack) del self.need_confirmation[:] + # Then send our packed request with BinaryWriter() as writer: request.on_send(writer) 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): - 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: - # TODO Is there any difference with unsigned long and long? - writer.write_long(self._session.salt, signed=False) - writer.write_long(self._session.id, signed=False) + writer.write_long(self.session.salt, signed=False) + writer.write_long(self.session.id, signed=False) writer.write_long(request.msg_id) writer.write_int(self.generate_sequence(request.confirmed)) writer.write_int(len(packet)) @@ -57,21 +76,22 @@ class MtProtoSender: 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) cipher_text = aes.encrypt(writer.get_bytes()) + # And then finally send the packet 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(cipher_text) - self._transport.send(writer.get_bytes()) + self.transport.send(writer.get_bytes()) def decode_msg(self, body): + """Decodes an received encrypted message body bytes""" message = None - remote_message_id = None + remote_msg_id = None remote_sequence = None with BinaryReader(body) as reader: @@ -82,72 +102,70 @@ class MtProtoSender: remote_auth_key_id = reader.read_long() 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(reader.read(len(body) - reader.tell_position())) with BinaryReader(plain_text) as plain_text_reader: remote_salt = 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() msg_len = plain_text_reader.read_int() 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): - while not mtproto_request.confirm_received: - 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): + def process_msg(self, msg_id, sequence, reader, request): + """Processes and handles a Telegram message""" # 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) reader.seek(-4) 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 - return self.handle_ping(message_id, sequence, reader) + return self.handle_ping(msg_id, sequence, reader) 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 - return self.handle_future_salts(message_id, sequence, reader) + return self.handle_future_salts(msg_id, sequence, reader) 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 - return self.handle_msgs_ack(message_id, sequence, reader) + return self.handle_msgs_ack(msg_id, sequence, reader) 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 - 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 - 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 - 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 - 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 - code == 0xd3f45784 or - code == 0x2b2fbd4e or - code == 0x78d4dec1 or - code == 0x725b04c3 or - code == 0x74ae4240): - return self.handle_update(message_id, sequence, reader) + code == 0xd3f45784 or + code == 0x2b2fbd4e or + code == 0x78d4dec1 or + code == 0x725b04c3 or + code == 0x74ae4240): + return self.handle_update(msg_id, sequence, reader) - # TODO Log unknown message code + print('Unknown message: {}'.format(hex(msg_id))) return False - def handle_update(self, message_id, sequence, reader): + # endregion + + # region Message handling + + def handle_update(self, msg_id, sequence, reader): 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) size = reader.read_int() for _ in range(size): @@ -156,7 +174,7 @@ class MtProtoSender: inner_length = reader.read_int() begin_position = reader.tell_position() 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) except: @@ -164,40 +182,40 @@ class MtProtoSender: return False - def handle_ping(self, message_id, sequence, reader): + def handle_ping(self, msg_id, sequence, reader): return False - def handle_pong(self, message_id, sequence, reader): + def handle_pong(self, msg_id, sequence, reader): 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) request_id = reader.read_long(signed=False) reader.seek(-12) 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 - def handle_msgs_ack(self, message_id, sequence, reader): + def handle_msgs_ack(self, msg_id, sequence, reader): 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) bad_msg_id = reader.read_long(signed=False) bad_msg_seq_no = reader.read_int() error_code = reader.read_int() new_salt = reader.read_long(signed=False) - self._session.salt = new_salt + self.session.salt = new_salt # Resend self.send(mtproto_request) 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) request_id = reader.read_long(signed=False) request_sequence = reader.read_int() @@ -238,14 +256,14 @@ class MtProtoSender: 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 - 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) 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 inner_code = reader.read_int(signed=False) @@ -273,7 +291,6 @@ class MtProtoSender: with BinaryReader(unpacked_data) as compressed_reader: mtproto_request.on_response(compressed_reader) - except: pass @@ -281,10 +298,13 @@ class MtProtoSender: reader.seek(-4) 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) packed_data = reader.tgread_bytes() unpacked_data = zlib.decompress(packed_data) 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 diff --git a/network/tcp_client.py b/network/tcp_client.py index ef1fe471..66d18a5a 100644 --- a/network/tcp_client.py +++ b/network/tcp_client.py @@ -1,20 +1,24 @@ +# Python rough implementation of a C# TCP client import socket class TcpClient: - def __init__(self): self.connected = False self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) def connect(self, ip, port): + """Connects to the specified IP and port number""" self.socket.connect((ip, port)) def close(self): + """Closes the connection""" self.socket.close() def write(self, data): + """Writes (sends) the specified bytes to the connected peer""" self.socket.send(data) def read(self, buffer_size): + """Reads (receives) the specified bytes from the connected peer""" self.socket.recv(buffer_size) diff --git a/network/tcp_message.py b/network/tcp_message.py index f08a8b45..e843db04 100644 --- a/network/tcp_message.py +++ b/network/tcp_message.py @@ -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 utils.binary_writer import BinaryWriter @@ -6,7 +7,6 @@ from utils.binary_reader import BinaryReader class TcpMessage: - def __init__(self, seq_number, body): """ :param seq_number: Sequence number @@ -19,6 +19,7 @@ class TcpMessage: self.body = body def encode(self): + """Returns the bytes of the this message encoded, following Telegram's guidelines""" with BinaryWriter() as writer: ''' https://core.telegram.org/mtproto#tcp-transport @@ -38,7 +39,9 @@ class TcpMessage: 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: raise ValueError('body cannot be None') diff --git a/network/tcp_transport.py b/network/tcp_transport.py index aa33a131..135fb5a3 100644 --- a/network/tcp_transport.py +++ b/network/tcp_transport.py @@ -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 network.tcp_message import TcpMessage from network.tcp_client import TcpClient class TcpTransport: - def __init__(self, ip_address, port): self._tcp_client = TcpClient() self._send_counter = 0 @@ -13,20 +13,20 @@ class TcpTransport: self._tcp_client.connect(ip_address, port) def send(self, packet): - """ - :param packet: Bytes array representing the packet to be sent - """ + """Sends the given packet (bytes array) to the connected peer""" if not self._tcp_client.connected: raise ConnectionError('Client not connected to server.') + # Get a TcpMessage which contains the given 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._send_counter += 1 def receive(self): + """Receives a TcpMessage from the connected peer""" + # First read everything packet_length_bytes = self._tcp_client.read(4) packet_length = int.from_bytes(packet_length_bytes, byteorder='big') @@ -45,6 +45,7 @@ class TcpTransport: if checksum != valid_checksum: raise ValueError('Invalid checksum, skip') + # If we passed the tests, we can then return a valid TcpMessage return TcpMessage(seq, body) def dispose(self): diff --git a/parser/source_builder.py b/parser/source_builder.py index 9011d2f5..5ad9d05e 100644 --- a/parser/source_builder.py +++ b/parser/source_builder.py @@ -1,23 +1,18 @@ -from io import StringIO - - class SourceBuilder: """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.on_new_line = False self.indent_size = indent_size - - if out_stream is None: - self.out_stream = StringIO() - else: - self.out_stream = out_stream + self.out_stream = out_stream def indent(self): + """Indents the current source code line by the current indentation level""" self.write(' ' * (self.current_indent * self.indent_size)) def write(self, string): + """Writes a string into the source code, applying indentation if required""" if self.on_new_line: 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 @@ -26,6 +21,7 @@ class SourceBuilder: self.out_stream.write(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.on_new_line = True @@ -34,6 +30,7 @@ class SourceBuilder: self.current_indent += 1 def end_block(self): + """Ends an indentation block, leaving an empty line afterwards""" self.current_indent -= 1 self.writeln() diff --git a/tl/generator.py b/parser/tl_generator.py similarity index 98% rename from tl/generator.py rename to parser/tl_generator.py index 955f92b0..529b6a4e 100644 --- a/tl/generator.py +++ b/parser/tl_generator.py @@ -1,6 +1,7 @@ import os import re import shutil + from parser.tl_parser import TLParser from parser.source_builder import SourceBuilder @@ -52,7 +53,7 @@ def generate_tlobjects(scheme_file): with open(filename, 'w', encoding='utf-8') as file: # Let's build the source code! 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('class {}(MTProtoRequest):'.format(get_class_name(tlobject))) @@ -346,8 +347,3 @@ def write_onresponse_code(builder, arg, args, name=None): if arg.is_flag: builder.end_block() - - -if __name__ == '__main__': - clean_tlobjects() - generate_tlobjects('scheme.tl') diff --git a/requests/__init__.py b/requests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/requests/ack_request.py b/requests/ack_request.py deleted file mode 100644 index 3f208f05..00000000 --- a/requests/ack_request.py +++ /dev/null @@ -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 diff --git a/tl/scheme.tl b/scheme.tl similarity index 100% rename from tl/scheme.tl rename to scheme.tl diff --git a/requests/mtproto_request.py b/tl/mtproto_request.py similarity index 88% rename from requests/mtproto_request.py rename to tl/mtproto_request.py index abdba99c..f642af79 100644 --- a/requests/mtproto_request.py +++ b/tl/mtproto_request.py @@ -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 diff --git a/utils/binary_reader.py b/utils/binary_reader.py index 1e7f9ff5..2fe3e738 100644 --- a/utils/binary_reader.py +++ b/utils/binary_reader.py @@ -21,18 +21,23 @@ class BinaryReader: # region Reading def read_int(self, signed=True): + """Reads an integer (4 bytes) value""" return int.from_bytes(self.reader.read(4), signed=signed, byteorder='big') 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') def read_large_int(self, bits): + """Reads a n-bits long integer value""" return int.from_bytes(self.reader.read(bits // 8), byteorder='big') def read(self, length): + """Read the given amount of bytes""" return self.reader.read(length) def get_bytes(self): + """Gets the byte array representing the current buffer as a whole""" return self.stream.getbuffer() # endregion @@ -40,6 +45,7 @@ class BinaryReader: # region Telegram custom reading def tgread_bytes(self): + """Reads a Telegram-encoded byte array, without the need of specifying its length""" first_byte = self.read(1) if first_byte == 254: length = self.read(1) | (self.read(1) << 8) | (self.read(1) << 16) @@ -56,6 +62,7 @@ class BinaryReader: return data def tgread_string(self): + """Reads a Telegram-encoded string""" return str(self.tgread_bytes(), encoding='utf-8') def tgread_object(self): diff --git a/utils/binary_writer.py b/utils/binary_writer.py index 0ddba440..f1c36e25 100644 --- a/utils/binary_writer.py +++ b/utils/binary_writer.py @@ -18,9 +18,11 @@ class BinaryWriter: # region Writing def write_byte(self, value): + """Writes a single byte value""" self.writer.write(pack('B', value)) def write_int(self, value, signed=True): + """Writes an integer value (4 bytes), which can or cannot be signed""" if signed: self.writer.write(pack('i', value)) else: @@ -28,6 +30,7 @@ class BinaryWriter: self.writer.write(pack('I', value)) def write_long(self, value, signed=True): + """Writes a long integer value (8 bytes), which can or cannot be signed""" if signed: self.writer.write(pack('q', value)) else: @@ -35,15 +38,19 @@ class BinaryWriter: self.writer.write(pack('Q', value)) def write_float(self, value): + """Writes a floating point value (4 bytes)""" self.writer.write(pack('f', value)) def write_double(self, value): + """Writes a floating point value (8 bytes)""" self.writer.write(pack('d', value)) def write_large_int(self, value, bits): + """Writes a n-bits long integer value""" self.writer.write(pack('{}B'.format(bits // 8), value)) def write(self, data): + """Writes the given bytes array""" self.writer.write(data) # endregion @@ -51,7 +58,7 @@ class BinaryWriter: # region Telegram custom writing def tgwrite_bytes(self, data): - + """Write bytes by using Telegram guidelines""" if len(data) < 254: padding = (len(data) + 1) % 4 if padding != 0: @@ -71,7 +78,6 @@ class BinaryWriter: self.write(bytes([(len(data) >> 8) % 256])) self.write(bytes([(len(data) >> 16) % 256])) self.write(data) - """ Original: binaryWriter.Write((byte)254); binaryWriter.Write((byte)(bytes.Length)); @@ -82,22 +88,27 @@ class BinaryWriter: self.write(bytes(padding)) def tgwrite_string(self, string): + """Write a string by using Telegram guidelines""" return self.tgwrite_bytes(string.encode('utf-8')) def tgwrite_bool(self, bool): + """Write a boolean value by using Telegram guidelines""" # boolTrue boolFalse return self.write_int(0x997275b5 if bool else 0xbc799737, signed=False) # endregion def flush(self): + """Flush the current stream to "update" changes""" self.writer.flush() def close(self): + """Close the current stream""" self.writer.close() # TODO Do I need to close the underlying stream? def get_bytes(self, flush=True): + """Get the current bytes array content from the buffer, optionally flushing first""" if flush: self.writer.flush() self.stream.getbuffer() diff --git a/utils/helpers.py b/utils/helpers.py index 39ef37ea..29a88876 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -4,6 +4,7 @@ from hashlib import sha1 def generate_random_long(signed=True): + """Generates a random long integer (8 bytes), which is optionally signed""" result = random.getrandbits(64) if not signed: result &= 0xFFFFFFFFFFFFFFFF # Ensure it's unsigned @@ -12,6 +13,7 @@ def generate_random_long(signed=True): def generate_random_bytes(count): + """Generates a random bytes array""" with BinaryWriter() as writer: for _ in range(count): writer.write(random.getrandbits(8)) @@ -20,6 +22,7 @@ def generate_random_bytes(count): 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 buffer = [0] * 48 @@ -47,10 +50,12 @@ def calc_key(shared_key, msg_key, client): def calc_msg_key(data): + """Calculates the message key from the given data""" return sha1(data)[4:20] 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 # In the original code it was as parameters for the sha function, not slicing the array return sha1(data[offset:offset + limit])[4:20]