diff --git a/README.md b/README.md index bd6ba817..b7106d20 100644 --- a/README.md +++ b/README.md @@ -24,5 +24,6 @@ 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 diff --git a/scheme.tl b/scheme.tl index 16f943ba..db060f49 100644 --- a/scheme.tl +++ b/scheme.tl @@ -403,7 +403,7 @@ updateShortMessage#914fbf11 flags:# out:flags.1?true mentioned:flags.4?true medi updateShortChatMessage#16812688 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int from_id:int chat_id:int message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int entities:flags.7?Vector = Updates; updateShort#78d4dec1 update:Update date:int = Updates; updatesCombined#725b04c3 updates:Vector users:Vector chats:Vector date:int seq_start:int seq:int = Updates; -updates#74ae4240 updates:Vector users:Vector chats:Vector date:int seq:int = Updates; +updates_tg#74ae4240 updates:Vector users:Vector chats:Vector date:int seq:int = Updates; updateShortSentMessage#11f1331c flags:# out:flags.1?true id:int pts:int pts_count:int date:int media:flags.9?MessageMedia entities:flags.7?Vector = Updates; photos.photos#8dca6aa5 photos:Vector users:Vector = photos.Photos; diff --git a/tl/tlobject.py b/tl/tlobject.py index 9b5232a0..855b3470 100644 --- a/tl/tlobject.py +++ b/tl/tlobject.py @@ -94,7 +94,10 @@ class TLArg: :param generic_definition: Is the argument a generic definition? (i.e. {X:Type}) """ - self.name = name + if name == 'self': # This very only name is restricted + self.name = 'is_self' + else: + self.name = name # Default values self.is_vector = False diff --git a/tlobjects_generator.py b/tlobjects_generator.py index 6f42fd41..bc926f90 100644 --- a/tlobjects_generator.py +++ b/tlobjects_generator.py @@ -10,8 +10,9 @@ def generate_tlobjecs(): # First ensure that the required parent directories exist os.makedirs('tl/functions', exist_ok=True) os.makedirs('tl/types', exist_ok=True) - for tlobject in TLParser.parse_file('scheme.tl'): + tlobjects = tuple(TLParser.parse_file('scheme.tl')) + for tlobject in tlobjects: # Determine the output directory and create it out_dir = os.path.join('tl', 'functions' if tlobject.is_function @@ -28,9 +29,8 @@ def generate_tlobjecs(): open(init_py, 'a').close() # Create the file - filename = os.path.join(out_dir, get_file_name(tlobject)) + filename = os.path.join(out_dir, get_file_name(tlobject, add_extension=True)) 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') @@ -79,6 +79,10 @@ def generate_tlobjecs(): builder.writeln('"""') builder.writeln('super().__init__()') + # Functions have a result object + if tlobject.is_function: + builder.writeln('self.result = None') + # Leave an empty line if there are any args if args: builder.writeln() @@ -96,6 +100,45 @@ def generate_tlobjecs(): write_onsend_code(builder, arg, tlobject.args) builder.end_block() + # Write the on_response(self, reader) function + builder.writeln('def on_response(self, reader):') + # Do not read constructor's ID, since that's already been read somewhere else + if tlobject.is_function: + builder.writeln('self.result = reader.tgread_object()') + else: + if tlobject.args: + for arg in tlobject.args: + write_onresponse_code(builder, arg, tlobject.args) + else: + builder.writeln('pass') + builder.end_block() + + # Once all the objects have been generated, we can now group them in a single file + filename = os.path.join('tl', 'all_tlobjects.py') + with open(filename, 'w', encoding='utf-8') as file: + with SourceBuilder(file) as builder: + builder.writeln('"""File generated by TLObjects\' generator. All changes will be ERASED"""') + builder.writeln() + + # First add imports + for tlobject in tlobjects: + builder.writeln('import {}'.format(get_full_file_name(tlobject))) + builder.writeln() + + # Then create the dictionary containing constructor_id: class + builder.writeln('tlobjects = {') + builder.current_indent += 1 + + for tlobject in tlobjects: + builder.writeln('{}: {}.{},'.format( + hex(tlobject.id), + get_full_file_name(tlobject), + get_class_name(tlobject) + )) + + builder.current_indent -= 1 + builder.writeln('}') + def get_class_name(tlobject): # Courtesy of http://stackoverflow.com/a/31531797/4759433 @@ -104,17 +147,30 @@ def get_class_name(tlobject): return result[:1].upper() + result[1:].replace('_', '') # Replace again to fully ensure! -def get_file_name(tlobject): +def get_full_file_name(tlobject): + fullname = get_file_name(tlobject, add_extension=False) + if tlobject.namespace is not None: + fullname = '{}.{}'.format(tlobject.namespace, fullname) + + if tlobject.is_function: + return 'tl.functions.{}'.format(fullname) + else: + return 'tl.types.{}'.format(fullname) + + +def get_file_name(tlobject, add_extension): # Courtesy of http://stackoverflow.com/a/1176023/4759433 s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', tlobject.name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + '.py' + result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + if add_extension: + return result + '.py' + else: + return result -foundEver = set() def write_onsend_code(builder, arg, args, name=None): """ Writes the write code for the given argument - :param builder: The source code builder :param arg: The argument to write :param args: All the other arguments in TLObject same on_send. This is required to determine the flags value @@ -183,9 +239,6 @@ def write_onsend_code(builder, arg, args, name=None): else: # Else it may be a custom type builder.writeln('{}.write(writer)'.format(name)) - if arg.type not in foundEver: - foundEver.add(arg.type) - print('{}: {}'.format(arg.type, arg)) # End vector and flag blocks if required (if we opened them before) if arg.is_vector: @@ -195,23 +248,81 @@ def write_onsend_code(builder, arg, args, name=None): builder.end_block() -''' SourceBuilder generated file example: +def write_onresponse_code(builder, arg, args, name=None): + """ + Writes the receive code for the given argument -class Example(MTProtoRequest): - def __init__(self, some, parameter): - """ - .tl definition: Example#12345678 some:int parameter:int = Exmpl - :param some: [type=Vector] Cannot be NONE - :param parameter: [type=int] Cannot be NONE - """ + :param builder: The source code builder + :param arg: The argument to write + :param args: All the other arguments in TLObject same on_send. This is required to determine the flags value + :param name: The name of the argument. Defaults to «self.argname» + This argument is an option because it's required when writing Vectors<> + """ - def on_send(self, writer): - writer.write_int(0x62d6b459) # example's constructor ID - writer.write_int(0x1cb5c415) # vector code - writer.write_int(len(self.msgs)) - for some_item in self.some: - writer.write_int(some_item) + if arg.generic_definition: + return # Do nothing, this only specifies a later type - def on_response(self, reader): - pass -''' + if name is None: + name = 'self.{}'.format(arg.name) + + # The argument may be a flag, only write that flag was given! + if arg.is_flag: + builder.writeln('if (flags & (1 << {})) != 0:'.format(arg.flag_index)) + + if arg.is_vector: + builder.writeln("reader.read_int() # Vector's constructor ID") + builder.writeln('{} = [] # Initialize an empty list'.format(name)) + builder.writeln('{}_len = reader.read_int()'.format(name)) + builder.writeln('for _ in range({}_len):'.format(name)) + # Temporary disable .is_vector, not to enter this if again + arg.is_vector = False + write_onresponse_code(builder, arg, args, name='{}_item'.format(arg.name)) + builder.writeln('{}.append({}_item)'.format(name, arg.name)) + arg.is_vector = True + + elif arg.flag_indicator: + # Read the flags, which will indicate what items we should read next + builder.writeln('flags = reader.read_int()') + builder.writeln() + + elif 'int' == arg.type: + builder.writeln('{} = reader.read_int()'.format(name)) + + elif 'long' == arg.type: + builder.writeln('{} = reader.read_long()'.format(name)) + + elif 'int128' == arg.type: + builder.writeln('{} = reader.read_large_int(bits=128)'.format(name)) + + elif 'int256' == arg.type: + builder.writeln('{} = reader.read_large_int(bits=256)'.format(name)) + + elif 'double' == arg.type: + builder.writeln('{} = reader.read_double()'.format(name)) + + elif 'string' == arg.type: + builder.writeln('{} = reader.tgread_string()'.format(name)) + + elif 'Bool' == arg.type: + builder.writeln('{} = reader.tgread_bool()'.format(name)) + + elif 'true' == arg.type: # Awkwardly enough, Telegram has both bool and "true", used in flags + builder.writeln('{} = reader.read_int() == 0x3fedd339 # true'.format(name)) + + elif 'bytes' == arg.type: + builder.writeln('{} = reader.read()'.format(name)) + + else: + # Else it may be a custom type + builder.writeln('{} = reader.tgread_object(reader)'.format(name)) + + # End vector and flag blocks if required (if we opened them before) + if arg.is_vector: + builder.end_block() + + if arg.is_flag: + builder.end_block() + + +def get_code(tg_type, stream_name, arg_name): + function_name = 'write' if stream_name == 'writer' else 'read' diff --git a/utils/binary_reader.py b/utils/binary_reader.py index 86f67310..1e7f9ff5 100644 --- a/utils/binary_reader.py +++ b/utils/binary_reader.py @@ -1,4 +1,5 @@ from io import BytesIO, BufferedReader +from tl.all_tlobjects import tlobjects import os @@ -25,6 +26,9 @@ class BinaryReader: def read_long(self, signed=True): return int.from_bytes(self.reader.read(8), signed=signed, byteorder='big') + def read_large_int(self, bits): + return int.from_bytes(self.reader.read(bits // 8), byteorder='big') + def read(self, length): return self.reader.read(length) @@ -54,6 +58,19 @@ class BinaryReader: def tgread_string(self): return str(self.tgread_bytes(), encoding='utf-8') + def tgread_object(self): + """Reads a Telegram object""" + id = self.read_int() + clazz = tlobjects.get(id, None) + if clazz is None: + raise ImportError('Could not find a matching ID for the TLObject that was supposed to be read. ' + 'Found ID: {}'.format(hex(id))) + + # Instantiate the class and return the result + result = clazz() + result.on_response(self) + return result + # endregion def close(self):