Several updates, fixes and additions (TcpClient, MtProto...)

README.md was updated to reflect more useful information
More errors from the official Telegrm website have been added
MtProtoSender now handles updates (and doesn't crash!)
Fixes on TcpClient to be able to receive whole large packets
Updated scheme.tl to the layer 55
Session is now saved more often (to prevent damages from crashes)
Fixes to the code generator (generated invalid code for reading "bytes")
This commit is contained in:
Lonami 2016-09-06 18:54:49 +02:00
parent 7802fe5487
commit 6b8a347426
12 changed files with 262 additions and 83 deletions

View File

@ -15,6 +15,34 @@ Also, you need to obtain your both [API ID and Hash](my.telegram.org). Once you
the `settings_example` file, naming it `settings` (lowercase!). Then fill the file with the corresponding values (your `api_id`,
`api_hash` and phone number in international format). Now it is when you're ready to go!
### How to add more functions to TelegramClient
As of now, you cannot call any Telegram function unless you first write it by hand under `tl/telegram_client.py`. Why?
Every Telegram function (or _request_) work in its own way. In some, you may only be interested in a single result field,
and in others you may need to format the result in a different way. However, a plan for the future is to be able to call
any function by giving its `namespace.name` and passing the arguments. But until that happens, to add a new function do:
1. Have a look under `tl/functions/` and find the `Request` that suits your needs.
2. Have a look inside that `Request` you chose, and find what arguments and in what order you'll need to call it.
3. Import it in `tl/telegram_client.py` by using `from tl.functions import SomeTelegramRequest`.
4. Add a new method, or function, that looks as follows:
```python
def my_function(self, my_arguments):
request = SomeTelegramRequest(my_arguments)
self.sender.send(request)
self.sender.receive(request)
return request.result
```
5. To determine how the result will look like, simply look at the original `.tl` definition. After the `=`,
you will see the type. Let's see an example:
`stickerPack#12b299d4 emoticon:string documents:Vector<long> = StickerPack;`
As it turns out, the result is going to be an `StickerPack`. Without a second doubt, head into `tl/types/` and find it;
open the file and see what the result will look like. Alternatively, you can simply `print(str(request.result))`!
Be warned that there may be more than one different type on the results. This is due to Telegram's polymorphism,
for example, a message may or not be empty, etc.
### Plans for the future
If everything works well, this probably ends up being a Python package :)

126
errors.py
View File

@ -1,3 +1,6 @@
import re
class InvalidParameterError(Exception):
"""Occurs when an invalid parameter is given, for example,
when either A or B are required but none is given"""
@ -19,7 +22,7 @@ class InvalidDCError(Exception):
def __init__(self, new_dc):
super().__init__(self, 'Your phone number is registered to #{} DC. '
'This should have been handled automatically; '
'if it has not, please restart the app.')
'if it has not, please restart the app.'.format(new_dc))
self.new_dc = new_dc
@ -34,9 +37,126 @@ class InvalidChecksumError(Exception):
class RPCError(Exception):
def __init__(self, message):
super().__init__(self, message)
CodeMessages = {
303: ('ERROR_SEE_OTHER', 'The request must be repeated, but directed to a different data center.'),
400: ('BAD_REQUEST', 'The query contains errors. In the event that a request was created using a '
'form and contains user generated data, the user should be notified that the '
'data must be corrected before the query is repeated.'),
401: ('UNAUTHORIZED', 'There was an unauthorized attempt to use functionality available only to '
'authorized users.'),
403: ('FORBIDDEN', 'Privacy violation. For example, an attempt to write a message to someone who '
'has blacklisted the current user.'),
404: ('NOT_FOUND', 'An attempt to invoke a non-existent object, such as a method.'),
420: ('FLOOD', 'The maximum allowed number of attempts to invoke the given method with '
'the given input parameters has been exceeded. For example, in an attempt '
'to request a large number of text messages (SMS) for the same phone number.'),
500: ('INTERNAL', 'An internal server error occurred while a request was being processed; '
'for example, there was a disruption while accessing a database or file storage.')
}
ErrorMessages = {
# 303 ERROR_SEE_OTHER
'FILE_MIGRATE_(\d+)': 'The file to be accessed is currently stored in a different data center (#{}).',
'PHONE_MIGRATE_(\d+)': 'The phone number a user is trying to use for authorization is associated '
'with a different data center (#{}).',
'NETWORK_MIGRATE_(\d+)': 'The source IP address is associated with a different data center (#{}, '
'for registration).',
'USER_MIGRATE_(\d+)': 'The user whose identity is being used to execute queries is associated with '
'a different data center (#{} for registration).',
# 400 BAD_REQUEST
'FIRSTNAME_INVALID': 'The first name is invalid.',
'LASTNAME_INVALID': 'The last name is invalid.',
'PHONE_NUMBER_INVALID': 'The phone number is invalid.',
'PHONE_CODE_HASH_EMPTY': 'phone_code_hash is missing.',
'PHONE_CODE_EMPTY': 'phone_code is missing.',
'PHONE_CODE_EXPIRED': 'The confirmation code has expired.',
'API_ID_INVALID': 'The api_id/api_hash combination is invalid.',
'PHONE_NUMBER_OCCUPIED': 'The phone number is already in use.',
'PHONE_NUMBER_UNOCCUPIED': 'The phone number is not yet being used.',
'USERS_TOO_FEW': 'Not enough users (to create a chat, for example).',
'USERS_TOO_MUCH': 'The maximum number of users has been exceeded (to create a chat, for example).',
'TYPE_CONSTRUCTOR_INVALID': 'The type constructor is invalid.',
'FILE_PART_INVALID': 'The file part number is invalid.',
'FILE_PARTS_INVALID': 'The number of file parts is invalid.',
'FILE_PART_(\d+)_MISSING': 'Part {} of the file is missing from storage.',
'MD5_CHECKSUM_INVALID': 'The MD5 checksums do not match.',
'PHOTO_INVALID_DIMENSIONS': 'The photo dimensions are invalid.',
'FIELD_NAME_INVALID': 'The field with the name FIELD_NAME is invalid.',
'FIELD_NAME_EMPTY': 'The field with the name FIELD_NAME is missing.',
'MSG_WAIT_FAILED': 'A waiting call returned an error.',
# 401 UNAUTHORIZED
'AUTH_KEY_UNREGISTERED': 'The key is not registered in the system.',
'AUTH_KEY_INVALID': 'The key is invalid.',
'USER_DEACTIVATED': 'The user has been deleted/deactivated.',
'SESSION_REVOKED': 'The authorization has been invalidated, because of the user terminating all sessions.',
'SESSION_EXPIRED': 'The authorization has expired.',
'ACTIVE_USER_REQUIRED': 'The method is only available to already activated users.',
'AUTH_KEY_PERM_EMPTY': 'The method is unavailable for temporary authorization key, not bound to permanent.',
# 420 FLOOD
'FLOOD_WAIT_(\d+)': 'A wait of {} seconds is required.'
}
def __init__(self, code, message):
self.code = code
self.code_meaning = RPCError.CodeMessages[code]
self.message = message
self.must_resend = code == 303 # ERROR_SEE_OTHER, "The request must be repeated"
called_super = False
for key, error_msg in RPCError.ErrorMessages.items():
match = re.match(key, message)
if match:
# Get additional_data, if any
if match.groups():
self.additional_data = int(match.group(1))
else:
self.additional_data = None
super().__init__(self, error_msg)
called_super = True
break
if not called_super:
super().__init__(self, 'Unknown error message with code {}: {}'.format(code, message))
class BadMessageError(Exception):

12
main.py
View File

@ -10,15 +10,17 @@ if __name__ == '__main__':
else:
settings = load_settings()
client = TelegramClient(session_user_id=settings.get('session_name', 'anonymous'),
layer=54,
layer=55,
api_id=settings['api_id'],
api_hash=settings['api_hash'])
client.connect()
input('You should now be connected. Press enter when you are ready to continue.')
if not client.is_user_authorized():
phone_code_hash = None
while phone_code_hash is None:
phone_code_hash = client.send_code_request(settings['user_phone'])
client.send_code_request(str(settings['user_phone']))
code = input('Enter the code you just received: ')
client.make_auth(settings['user_phone'], phone_code_hash, code)
client.make_auth(settings['user_phone'], code)
else:
client.get_dialogs()

View File

@ -166,7 +166,6 @@ def do_authentication(transport):
sender.send(set_client_dh_params_bytes)
# Step 3 response: Complete DH Exchange
# TODO, no more data, why did it stop again?!
with BinaryReader(sender.receive()) as reader:
code = reader.read_int(signed=False)
if code == 0x3bcbf734: # DH Gen OK

View File

@ -17,6 +17,12 @@ class MtProtoSender:
self.transport = transport
self.session = session
self.need_confirmation = [] # Message IDs that need confirmation
self.on_update_handlers = []
def add_update_handler(self, handler):
"""Adds an update handler (a method with one argument, the received TLObject)
that is fired when there are updates available"""
self.on_update_handlers.append(handler)
def generate_sequence(self, confirmed):
"""Generates the next sequence number, based on whether it was confirmed yet or not"""
@ -29,7 +35,6 @@ class MtProtoSender:
# region Send and receive
# TODO In TLSharp, this was async. Should this be?
def send(self, request):
"""Sends the specified MTProtoRequest, previously sending any message which needed confirmation"""
@ -57,6 +62,7 @@ class MtProtoSender:
with BinaryReader(message) as reader:
self.process_msg(remote_msg_id, remote_sequence, reader, request)
# endregion
# region Low level processing
@ -144,12 +150,9 @@ class MtProtoSender:
if code == 0x3072cfa1: # gzip_packed
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):
# TODO do not check by hand, keep another list of which are updates (from the .tl definition)
updates = [0xe317af7e, 0x914fbf11, 0x16812688, 0x78d4dec1, 0x725b04c3, 0x74ae4240, 0x11f1331c]
if code in updates:
return self.handle_update(msg_id, sequence, reader)
print('Unknown message: {}'.format(hex(msg_id)))
@ -160,6 +163,10 @@ class MtProtoSender:
# region Message handling
def handle_update(self, msg_id, sequence, reader):
tlobject = reader.tgread_object()
for handler in self.on_update_handlers:
handler(tlobject)
return False
def handle_container(self, msg_id, sequence, reader, request):
@ -170,17 +177,9 @@ class MtProtoSender:
inner_sequence = reader.read_int()
inner_length = reader.read_int()
begin_position = reader.tell_position()
try:
if not self.process_msg(inner_msg_id, sequence, reader, request):
reader.set_position(begin_position + inner_length)
except Exception as error:
if error is InvalidDCError:
print('Re-raise {}'.format(error))
raise error
else:
print('Error while handling container {}: {}'.format(type(error), error))
reader.set_position(begin_position + inner_length)
if not self.process_msg(inner_msg_id, sequence, reader, request):
reader.set_position(begin_position + inner_length)
return False
@ -231,34 +230,35 @@ class MtProtoSender:
def handle_rpc_result(self, msg_id, sequence, reader, mtproto_request):
code = reader.read_int(signed=False)
request_id = reader.read_long(signed=False)
inner_code = reader.read_int(signed=False)
if request_id == mtproto_request.msg_id:
mtproto_request.confirm_received = True
inner_code = reader.read_int(signed=False)
if inner_code == 0x2144ca19: # RPC Error
error_code = reader.read_int()
error_msg = reader.tgread_string()
error = RPCError(code=reader.read_int(), message=reader.tgread_string())
if error.must_resend:
mtproto_request.confirm_received = False
if error_msg.startswith('FLOOD_WAIT_'):
seconds = int(re.search(r'\d+', error_msg).group(0))
print('Should wait {}s. Sleeping until then.')
sleep(seconds)
if error.message.startswith('FLOOD_WAIT_'):
print('Should wait {}s. Sleeping until then.'.format(error.additional_data))
sleep(error.additional_data)
elif error.message.startswith('PHONE_MIGRATE_'):
raise InvalidDCError(error.additional_data)
elif error_msg.startswith('PHONE_MIGRATE_'):
dc_index = int(re.search(r'\d+', error_msg).group(0))
raise InvalidDCError(dc_index)
else:
raise RPCError(error_msg)
elif inner_code == 0x3072cfa1: # GZip packed
unpacked_data = gzip.decompress(reader.tgread_bytes())
with BinaryReader(unpacked_data) as compressed_reader:
mtproto_request.on_response(compressed_reader)
raise error
else:
reader.seek(-4)
mtproto_request.on_response(reader)
if inner_code == 0x3072cfa1: # GZip packed
unpacked_data = gzip.decompress(reader.tgread_bytes())
with BinaryReader(unpacked_data) as compressed_reader:
mtproto_request.on_response(compressed_reader)
else:
reader.seek(-4)
mtproto_request.on_response(reader)
def handle_gzip_packed(self, msg_id, sequence, reader, mtproto_request):
code = reader.read_int(signed=False)

View File

@ -23,4 +23,11 @@ class TcpClient:
def read(self, buffer_size):
"""Reads (receives) the specified bytes from the connected peer"""
return self.socket.recv(buffer_size)
# TODO improve (don't cast to list, use a mutable byte list instead or similar, see recv_into)
result = []
while len(result) < buffer_size:
left_data = buffer_size - len(result)
partial = self.socket.recv(left_data)
result.extend(list(partial))
return bytes(result)

View File

@ -20,7 +20,6 @@ class TcpTransport:
# Get a TcpMessage which contains the given packet
tcp_message = TcpMessage(self._send_counter, packet)
# 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
@ -48,6 +47,6 @@ class TcpTransport:
# If we passed the tests, we can then return a valid TcpMessage
return TcpMessage(seq, body)
def dispose(self):
def close(self):
if self._tcp_client.connected:
self._tcp_client.close()

View File

@ -155,6 +155,8 @@ inputMediaUploadedThumbDocument#ad613491 file:InputFile thumb:InputFile mime_typ
inputMediaDocument#1a77f29c id:InputDocument caption:string = InputMedia;
inputMediaVenue#2827a81a geo_point:InputGeoPoint title:string address:string provider:string venue_id:string = InputMedia;
inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
inputMediaPhotoExternal#b55f4f18 url:string caption:string = InputMedia;
inputMediaDocumentExternal#e5e9607c url:string caption:string = InputMedia;
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
inputChatUploadedPhoto#94254732 file:InputFile crop:InputPhotoCrop = InputChatPhoto;
@ -268,7 +270,7 @@ auth.checkedPhone#811ea28e phone_registered:Bool = auth.CheckedPhone;
auth.sentCode#5e002502 flags:# phone_registered:flags.0?true type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode;
auth.authorization#ff036af1 user:User = auth.Authorization;
auth.authorization#cd050916 flags:# tmp_sessions:flags.0?int user:User = auth.Authorization;
auth.exportedAuthorization#df969c2d id:int bytes:bytes = auth.ExportedAuthorization;
@ -394,6 +396,8 @@ updateReadChannelOutbox#25d6c9c7 channel_id:int max_id:int = Update;
updateDraftMessage#ee2bb969 peer:Peer draft:DraftMessage = Update;
updateReadFeaturedStickers#571d2742 = Update;
updateRecentStickers#9a422c20 = Update;
updateConfig#a229dd06 = Update;
updatePtsChanged#3354678f = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@ -418,7 +422,7 @@ upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File;
dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true id:int ip_address:string port:int = DcOption;
config#f401a4bf date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int disabled_features:Vector<DisabledFeature> = Config;
config#9a6b2e2a flags:# date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int tmp_sessions:flags.0?int disabled_features:Vector<DisabledFeature> = Config;
nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;
@ -568,7 +572,7 @@ keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton;
keyboardButtonCallback#683a5e46 text:string data:bytes = KeyboardButton;
keyboardButtonRequestPhone#b16a6c29 text:string = KeyboardButton;
keyboardButtonRequestGeoLocation#fc796b3f text:string = KeyboardButton;
keyboardButtonSwitchInline#ea1b7a14 text:string query:string = KeyboardButton;
keyboardButtonSwitchInline#568a748 flags:# same_peer:flags.0?true text:string query:string = KeyboardButton;
keyboardButtonRow#77608b83 buttons:Vector<KeyboardButton> = KeyboardButtonRow;
@ -672,7 +676,7 @@ auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType;
auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType;
auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType;
messages.botCallbackAnswer#b10df1fb flags:# alert:flags.1?true message:flags.0?string url:flags.2?string = messages.BotCallbackAnswer;
messages.botCallbackAnswer#b10df1fb flags:# alert:flags.1?true has_url:flags.3?true message:flags.0?string url:flags.2?string = messages.BotCallbackAnswer;
messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData;
@ -804,7 +808,6 @@ messages.addChatUser#f9a0aa09 chat_id:int user_id:InputUser fwd_limit:int = Upda
messages.deleteChatUser#e0611f16 chat_id:int user_id:InputUser = Updates;
messages.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
messages.forwardMessage#33963bf9 peer:InputPeer id:int random_id:long = Updates;
messages.sendBroadcast#bf73f4da contacts:Vector<InputUser> random_id:Vector<long> message:string media:InputMedia = Updates;
messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;
messages.requestEncryption#f64daf43 user_id:InputUser random_id:int g_a:bytes = EncryptedChat;
messages.acceptEncryption#3dbc0415 peer:InputEncryptedChat g_b:bytes key_fingerprint:long = EncryptedChat;
@ -903,5 +906,6 @@ channels.toggleInvites#49609307 channel:InputChannel enabled:Bool = Updates;
channels.exportMessageLink#c846d22d channel:InputChannel id:int = ExportedMessageLink;
channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates;
channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates;
channels.getAdminedPublicChannels#8d8d82d7 = messages.Chats;
// LAYER 54
// LAYER 55

View File

@ -1,6 +1,5 @@
# This file is based on TLSharp
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/TelegramClient.cs
import re
import platform
import utils
@ -9,11 +8,11 @@ from network import MtProtoSender, TcpTransport
from errors import *
from tl import Session
from tl.types import InputPeerUser
from tl.types import InputPeerUser, InputPeerEmpty
from tl.functions import InvokeWithLayerRequest, InitConnectionRequest
from tl.functions.help import GetConfigRequest
from tl.functions.auth import CheckPhoneRequest, SendCodeRequest, SignInRequest
from tl.functions.messages import SendMessageRequest
from tl.functions.messages import GetDialogsRequest, SendMessageRequest
class TelegramClient:
@ -32,28 +31,32 @@ class TelegramClient:
# These will be set later
self.dc_options = None
self.sender = None
self.phone_code_hash = None
# TODO Should this be async?
def connect(self, reconnect=False):
if not self.session.auth_key or reconnect:
self.session.auth_key, self.session.time_offset = network.authenticator.do_authentication(self.transport)
self.session.save()
self.sender = MtProtoSender(self.transport, self.session)
self.sender.add_update_handler(self.on_update)
if not reconnect:
request = InvokeWithLayerRequest(layer=self.layer,
query=InitConnectionRequest(api_id=self.api_id,
device_model=platform.node(),
system_version=platform.system(),
app_version='0.1',
lang_code='en',
query=GetConfigRequest()))
# Always init connection by using the latest layer, not only when not reconnecting (as in original TLSharp's)
# Otherwise, the server thinks that were using the oldest layer!
# (Note that this is mainly untested, but it seems like it since some errors point in that direction)
request = InvokeWithLayerRequest(layer=self.layer,
query=InitConnectionRequest(api_id=self.api_id,
device_model=platform.node(),
system_version=platform.system(),
app_version='0.1',
lang_code='en',
query=GetConfigRequest()))
self.sender.send(request)
self.sender.receive(request)
self.sender.send(request)
self.sender.receive(request)
# Result is a Config TLObject
self.dc_options = request.result.dc_options
# Result is a Config TLObject
self.dc_options = request.result.dc_options
return True
@ -64,9 +67,11 @@ class TelegramClient:
# dc is a DcOption TLObject
dc = next(dc for dc in self.dc_options if dc.id == dc_id)
self.transport.close()
self.transport = TcpTransport(dc.ip_address, dc.port)
self.session.server_address = dc.ip_address
self.session.port = dc.port
self.session.save()
self.connect(reconnect=True)
@ -84,7 +89,6 @@ class TelegramClient:
return request.result.phone_registered
def send_code_request(self, phone_number):
"""May return None if an error occured!"""
request = SendCodeRequest(phone_number, self.api_id, self.api_hash)
completed = False
while not completed:
@ -92,17 +96,17 @@ class TelegramClient:
self.sender.send(request)
self.sender.receive(request)
completed = True
if request.result:
self.phone_code_hash = request.result.phone_code_hash
except InvalidDCError as error:
self.reconnect_to_dc(error.new_dc)
if request.result is None:
return None
else:
return request.result.phone_code_hash
def make_auth(self, phone_number, code):
if not self.phone_code_hash:
raise ValueError('Please make sure you have called send_code_request first!')
def make_auth(self, phone_number, phone_code_hash, code):
request = SignInRequest(phone_number, phone_code_hash, code)
request = SignInRequest(phone_number, self.phone_code_hash, code)
self.sender.send(request)
self.sender.receive(request)
@ -112,9 +116,25 @@ class TelegramClient:
return self.session.user
def get_dialogs(self):
request = GetDialogsRequest(offset_date=0,
offset_id=0,
offset_peer=InputPeerEmpty(),
limit=20)
self.sender.send(request)
self.sender.receive(request)
print(request.result)
def send_message(self, user, message):
peer = InputPeerUser(user.id, user.access_hash)
request = SendMessageRequest(peer, message, utils.generate_random_long())
self.sender.send(request)
self.sender.send(request)
self.sender.receive(request)
def on_update(self, tlobject):
"""This method is fired when there are updates from Telegram.
Add your own implementation below, or simply override it!"""
print('We have an update: {}'.format(str(tlobject)))

View File

@ -93,8 +93,8 @@ class TLObject:
valid_args = [arg for arg in self.args
if not arg.flag_indicator and not arg.generic_definition]
args = ', '.join(['{} = {{}}'.format(arg.name) for arg in valid_args])
args_format = ', '.join(['self.{}'.format(arg.name) for arg in valid_args])
args = ', '.join(['{}={{}}'.format(arg.name) for arg in valid_args])
args_format = ', '.join(['str(self.{})'.format(arg.name) for arg in valid_args])
return ("'({} (ID: {}) = ({}))'.format({})"
.format(fullname, hex(self.id), args, args_format))

View File

@ -63,7 +63,7 @@ def generate_tlobjects(scheme_file):
# Write the original .tl definition, along with a "generated automatically" message
builder.writeln('"""Class generated by TLObjects\' generator. '
'All changes will be ERASED. Original .tl definition below.')
builder.writeln('{}"""'.format(tlobject))
builder.writeln('{}"""'.format(repr(tlobject)))
builder.writeln()
# First sort the arguments so that those not being a flag come first
@ -359,7 +359,7 @@ def write_onresponse_code(builder, arg, args, name=None):
builder.writeln('{} = True # Arbitrary not-None value, no need to read since it is a flag'.format(name))
elif 'bytes' == arg.type:
builder.writeln('{} = reader.read()'.format(name))
builder.writeln('{} = reader.tgread_bytes()'.format(name))
else:
# Else it may be a custom type

View File

@ -267,7 +267,7 @@ class UnitTest(unittest.TestCase):
def test_authenticator():
transport = TcpTransport('149.154.167.91', 443)
network.authenticator.do_authentication(transport)
transport.dispose()
transport.close()
if __name__ == '__main__':
unittest.main()