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** 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 <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))
### 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!
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!

10
main.py
View File

@ -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:
if not parser.tl_generator.tlobjects_exist():
print('First run. Generating TLObjects...')
parser.tl_generator.generate_tlobjects('scheme.tl')
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
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

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 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,56 +102,50 @@ 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
@ -139,15 +153,19 @@ class MtProtoSender:
code == 0x78d4dec1 or
code == 0x725b04c3 or
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
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

View File

@ -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)

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 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')

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 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):

View File

@ -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
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()

View File

@ -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')

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

View File

@ -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):

View File

@ -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()

View File

@ -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]