diff --git a/run_tests.py b/run_tests.py index 0cdb8723..3fa12aff 100644 --- a/run_tests.py +++ b/run_tests.py @@ -1,6 +1,5 @@ import unittest - if __name__ == '__main__': from telethon_tests import CryptoTests, ParserTests, TLTests, UtilsTests, NetworkTests test_classes = [CryptoTests, ParserTests, TLTests, UtilsTests] diff --git a/setup.py b/setup.py index 49bda1cd..f3ec9173 100644 --- a/setup.py +++ b/setup.py @@ -5,14 +5,15 @@ https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject """ -from telethon import TelegramClient - -# Always prefer setuptools over distutils -from setuptools import setup, find_packages # To use a consistent encoding from codecs import open from os import path +# Always prefer setuptools over distutils +from setuptools import find_packages, setup + +from telethon import TelegramClient + here = path.abspath(path.dirname(__file__)) # Get the long description from the README file @@ -24,7 +25,6 @@ setup( # Versions should comply with PEP440. version=TelegramClient.__version__, - description="Python3 Telegram's client implementation with full access to its API", long_description=long_description, @@ -63,15 +63,14 @@ setup( ], # What does your project relate to? - keywords='telegram api chat client mtproto', + keywords='Telegram API chat client MTProto', # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). packages=find_packages(exclude=[ - 'telethon_generator', - 'telethon_tests', - 'run_tests.py', - 'try_telethon.py']), + 'telethon_generator', 'telethon_tests', 'run_tests.py', + 'try_telethon.py' + ]), # List run-time dependencies here. These will be installed by pip when # your project is installed. @@ -84,5 +83,4 @@ setup( 'console_scripts': [ 'gen_tl = tl_generator:clean_and_generate', ], - } -) + }) diff --git a/telethon/crypto/aes.py b/telethon/crypto/aes.py index 4f9054ad..c455c2b6 100644 --- a/telethon/crypto/aes.py +++ b/telethon/crypto/aes.py @@ -6,8 +6,8 @@ class AES: @staticmethod def decrypt_ige(cipher_text, key, iv): """Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector""" - iv1 = iv[:len(iv)//2] - iv2 = iv[len(iv)//2:] + iv1 = iv[:len(iv) // 2] + iv2 = iv[len(iv) // 2:] aes = pyaes.AES(key) @@ -17,7 +17,8 @@ class AES: cipher_text_block = [0] * 16 for block_index in range(blocks_count): for i in range(16): - cipher_text_block[i] = cipher_text[block_index * 16 + i] ^ iv2[i] + cipher_text_block[i] = cipher_text[block_index * 16 + i] ^ iv2[ + i] plain_text_block = aes.decrypt(cipher_text_block) @@ -40,8 +41,8 @@ class AES: padding_count = 16 - len(plain_text) % 16 plain_text += os.urandom(padding_count) - iv1 = iv[:len(iv)//2] - iv2 = iv[len(iv)//2:] + iv1 = iv[:len(iv) // 2] + iv2 = iv[len(iv) // 2:] aes = pyaes.AES(key) @@ -49,7 +50,8 @@ class AES: blocks_count = len(plain_text) // 16 for block_index in range(blocks_count): - plain_text_block = list(plain_text[block_index * 16:block_index * 16 + 16]) + plain_text_block = list(plain_text[block_index * 16:block_index * + 16 + 16]) for i in range(16): plain_text_block[i] ^= iv1[i] diff --git a/telethon/crypto/auth_key.py b/telethon/crypto/auth_key.py index 356add55..7643958c 100755 --- a/telethon/crypto/auth_key.py +++ b/telethon/crypto/auth_key.py @@ -1,5 +1,5 @@ -from telethon.utils import BinaryWriter, BinaryReader import telethon.helpers as utils +from telethon.utils import BinaryReader, BinaryWriter class AuthKey: diff --git a/telethon/crypto/rsa.py b/telethon/crypto/rsa.py index c71e914f..8cc7e437 100755 --- a/telethon/crypto/rsa.py +++ b/telethon/crypto/rsa.py @@ -1,7 +1,8 @@ -from telethon.utils import BinaryWriter -import telethon.helpers as utils import os +import telethon.helpers as utils +from telethon.utils import BinaryWriter + class RSAServerKey: def __init__(self, fingerprint, m, e): @@ -18,9 +19,9 @@ class RSAServerKey: with BinaryWriter() as writer: # Write SHA - writer.write(utils.sha1(data[offset:offset+length])) + writer.write(utils.sha1(data[offset:offset + length])) # Write data - writer.write(data[offset:offset+length]) + writer.write(data[offset:offset + length]) # Add padding if required if length < 235: writer.write(os.urandom(235 - length)) @@ -31,21 +32,22 @@ class RSAServerKey: # If the result byte count is less than 256, since the byte order is big, # the non-used bytes on the left will be 0 and act as padding, # without need of any additional checks - return int.to_bytes(result, length=256, byteorder='big', signed=False) + return int.to_bytes( + result, length=256, byteorder='big', signed=False) class RSA: _server_keys = { - '216be86c022bb4c3': - RSAServerKey('216be86c022bb4c3', int('C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9' - '1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E' - '580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F' - '9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934' - 'EF781D866B34F011FCE4D835A090196E9A5F0E4449AF7EB697DDB9076494CA5F' - '81104A305B6DD27665722C46B60E5DF680FB16B210607EF217652E60236C255F' - '6A28315F4083A96791D7214BF64C1DF4FD0DB1944FB26A2A57031B32EEE64AD1' - '5A8BA68885CDE74A5BFC920F6ABF59BA5C75506373E7130F9042DA922179251F', - 16), int('010001', 16)) + '216be86c022bb4c3': RSAServerKey('216be86c022bb4c3', int( + 'C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9' + '1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E' + '580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F' + '9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934' + 'EF781D866B34F011FCE4D835A090196E9A5F0E4449AF7EB697DDB9076494CA5F' + '81104A305B6DD27665722C46B60E5DF680FB16B210607EF217652E60236C255F' + '6A28315F4083A96791D7214BF64C1DF4FD0DB1944FB26A2A57031B32EEE64AD1' + '5A8BA68885CDE74A5BFC920F6ABF59BA5C75506373E7130F9042DA922179251F', + 16), int('010001', 16)) } @staticmethod diff --git a/telethon/errors.py b/telethon/errors.py index ac1f92df..06b168ab 100644 --- a/telethon/errors.py +++ b/telethon/errors.py @@ -3,6 +3,7 @@ import re class ReadCancelledError(Exception): """Occurs when a read operation was cancelled""" + def __init__(self): super().__init__(self, 'The read operation was cancelled.') @@ -15,28 +16,33 @@ class InvalidParameterError(Exception): class TypeNotFoundError(Exception): """Occurs when a type is not found, for example, when trying to read a TLObject with an invalid constructor code""" + def __init__(self, invalid_constructor_id): - super().__init__(self, 'Could not find a matching Constructor ID for the TLObject ' - 'that was supposed to be read with ID {}. Most likely, a TLObject ' - 'was trying to be read when it should not be read.' - .format(hex(invalid_constructor_id))) + super().__init__( + self, 'Could not find a matching Constructor ID for the TLObject ' + 'that was supposed to be read with ID {}. Most likely, a TLObject ' + 'was trying to be read when it should not be read.' + .format(hex(invalid_constructor_id))) self.invalid_constructor_id = invalid_constructor_id 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.'.format(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.'.format(new_dc)) self.new_dc = new_dc class InvalidChecksumError(Exception): def __init__(self, checksum, valid_checksum): - super().__init__(self, 'Invalid checksum ({} when {} was expected). This packet should be skipped.' - .format(checksum, valid_checksum)) + super().__init__( + self, + 'Invalid checksum ({} when {} was expected). This packet should be skipped.' + .format(checksum, valid_checksum)) self.checksum = checksum self.valid_checksum = valid_checksum @@ -45,105 +51,95 @@ class InvalidChecksumError(Exception): class RPCError(Exception): 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.') + 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).', + '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': 'The phone code hash is missing.', - 'PHONE_CODE_EMPTY': 'The phone code is missing.', - 'PHONE_CODE_INVALID': 'The phone code entered was invalid.', - '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).', - + '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.', - + '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.', - - 'CHAT_ADMIN_REQUIRED': 'Chat admin privileges are required to do that in the specified chat ' - '(for example, to send a message in a channel which is not yours).', - - 'PASSWORD_HASH_INVALID': 'The password (and thus its hash value) you entered is invalid.', + 'CHAT_ADMIN_REQUIRED': + 'Chat admin privileges are required to do that in the specified chat ' + '(for example, to send a message in a channel which is not yours).', + 'PASSWORD_HASH_INVALID': + 'The password (and thus its hash value) you entered is invalid.', # 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_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.', - - 'SESSION_PASSWORD_NEEDED': 'Two-steps verification is enabled and a password is required.', + '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.', + 'SESSION_PASSWORD_NEEDED': + 'Two-steps verification is enabled and a password is required.', # 420 FLOOD 'FLOOD_WAIT_(\d+)': 'A wait of {} seconds is required.' @@ -163,7 +159,8 @@ class RPCError(Exception): # Get additional_data, if any if match.groups(): self.additional_data = int(match.group(1)) - super().__init__(self, error_msg.format(self.additional_data)) + super().__init__(self, + error_msg.format(self.additional_data)) else: self.additional_data = None super().__init__(self, error_msg) @@ -176,47 +173,49 @@ class RPCError(Exception): break if not called_super: - super().__init__(self, 'Unknown error message with code {}: {}'.format(code, message)) + super().__init__( + self, 'Unknown error message with code {}: {}'.format(code, + message)) class BadMessageError(Exception): """Occurs when handling a bad_message_notification""" ErrorMessages = { - 16: 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' - 'synchronize it using msg_id notifications and re-send the original message ' - 'with the "correct" msg_id or wrap it in a container with a new msg_id if the ' - 'original message had waited too long on the client to be transmitted).', - - 17: 'msg_id too high (similar to the previous case, the client time has to be ' - 'synchronized, and the message re-sent with the correct msg_id).', - - 18: 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' - 'to be divisible by 4).', - - 19: 'Container msg_id is the same as msg_id of a previously received message ' - '(this must never happen).', - - 20: 'Message too old, and it cannot be verified whether the server has received a ' - 'message with this msg_id or not.', - - 32: 'msg_seqno too low (the server has already received a message with a lower ' - 'msg_id but with either a higher or an equal and odd seqno).', - - 33: 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' - 'either a lower or an equal and odd seqno).', - - 34: 'An even msg_seqno expected (irrelevant message), but odd received.', - + 16: + 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' + 'synchronize it using msg_id notifications and re-send the original message ' + 'with the "correct" msg_id or wrap it in a container with a new msg_id if the ' + 'original message had waited too long on the client to be transmitted).', + 17: + 'msg_id too high (similar to the previous case, the client time has to be ' + 'synchronized, and the message re-sent with the correct msg_id).', + 18: + 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' + 'to be divisible by 4).', + 19: + 'Container msg_id is the same as msg_id of a previously received message ' + '(this must never happen).', + 20: + 'Message too old, and it cannot be verified whether the server has received a ' + 'message with this msg_id or not.', + 32: + 'msg_seqno too low (the server has already received a message with a lower ' + 'msg_id but with either a higher or an equal and odd seqno).', + 33: + 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' + 'either a lower or an equal and odd seqno).', + 34: + 'An even msg_seqno expected (irrelevant message), but odd received.', 35: 'Odd msg_seqno expected (relevant message), but even received.', - - 48: 'Incorrect server salt (in this case, the bad_server_salt response is received with ' - 'the correct salt, and the message is to be re-sent with it).', - + 48: + 'Incorrect server salt (in this case, the bad_server_salt response is received with ' + 'the correct salt, and the message is to be re-sent with it).', 64: 'Invalid container.' } def __init__(self, code): - super().__init__(self, BadMessageError - .ErrorMessages.get(code,'Unknown error code (this should not happen): {}.'.format(code))) + super().__init__(self, BadMessageError.ErrorMessages.get( + code, + 'Unknown error code (this should not happen): {}.'.format(code))) self.code = code diff --git a/telethon/helpers.py b/telethon/helpers.py index 9eefd351..3dbffa7b 100755 --- a/telethon/helpers.py +++ b/telethon/helpers.py @@ -1,5 +1,5 @@ -import os import hashlib +import os # region Multiple utilities @@ -15,7 +15,6 @@ def ensure_parent_dir_exists(file_path): if parent: os.makedirs(parent, exist_ok=True) - # endregion # region Cryptographic related utils @@ -26,7 +25,8 @@ def calc_key(shared_key, msg_key, client): x = 0 if client else 8 sha1a = sha1(msg_key + shared_key[x:x + 32]) - sha1b = sha1(shared_key[x + 32:x + 48] + msg_key + shared_key[x + 48:x + 64]) + sha1b = sha1(shared_key[x + 32:x + 48] + msg_key + shared_key[x + 48:x + + 64]) sha1c = sha1(shared_key[x + 64:x + 96] + msg_key) sha1d = sha1(msg_key + shared_key[x + 96:x + 128]) @@ -74,8 +74,7 @@ def get_password_hash(pw, current_salt): # https://github.com/DrKLO/Telegram/blob/e31388/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java#L2003 data = pw.encode('utf-8') - pw_hash = current_salt+data+current_salt + pw_hash = current_salt + data + current_salt return sha256(pw_hash) - # endregion diff --git a/telethon/interactive_telegram_client.py b/telethon/interactive_telegram_client.py index 3ddfa1fa..ab9e8e0b 100644 --- a/telethon/interactive_telegram_client.py +++ b/telethon/interactive_telegram_client.py @@ -1,12 +1,10 @@ -from telethon.tl.types import UpdateShortChatMessage -from telethon.tl.types import UpdateShortMessage -from telethon import TelegramClient, RPCError - -from telethon.utils import get_display_name, get_input_peer - import shutil from getpass import getpass +from telethon import RPCError, TelegramClient +from telethon.tl.types import UpdateShortChatMessage, UpdateShortMessage +from telethon.utils import get_display_name, get_input_peer + # Get the (current) number of lines in the terminal cols, rows = shutil.get_terminal_size() @@ -27,7 +25,8 @@ def bytes_to_string(byte_count): byte_count /= 1024 suffix_index += 1 - return '{:.2f}{}'.format(byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]) + return '{:.2f}{}'.format(byte_count, + [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]) class InteractiveTelegramClient(TelegramClient): @@ -58,7 +57,8 @@ class InteractiveTelegramClient(TelegramClient): # Two-step verification may be enabled except RPCError as e: if e.password_required: - pw = getpass('Two step verification is enabled. Please enter your password: ') + pw = getpass( + 'Two step verification is enabled. Please enter your password: ') code_ok = self.sign_in(password=pw) else: raise e @@ -117,10 +117,14 @@ class InteractiveTelegramClient(TelegramClient): print('Available commands:') print(' !q: Quits the current chat.') print(' !Q: Quits the current chat and exits.') - print(' !h: prints the latest messages (message History) of the chat.') - print(' !up : Uploads and sends a Photo located at the given path.') - print(' !uf : Uploads and sends a File document located at the given path.') - print(' !dm : Downloads the given message Media (if any).') + print( + ' !h: prints the latest messages (message History) of the chat.') + print( + ' !up : Uploads and sends a Photo located at the given path.') + print( + ' !uf : Uploads and sends a File document located at the given path.') + print( + ' !dm : Downloads the given message Media (if any).') print(' !dp: Downloads the current dialog Profile picture.') print() @@ -136,10 +140,12 @@ class InteractiveTelegramClient(TelegramClient): # History elif msg == '!h': # First retrieve the messages and some information - total_count, messages, senders = self.get_message_history(input_peer, limit=10) + total_count, messages, senders = self.get_message_history( + input_peer, limit=10) # Iterate over all (in reverse order so the latest appears the last in the console) # and print them in "[hh:mm] Sender: Message" text format - for msg, sender in zip(reversed(messages), reversed(senders)): + for msg, sender in zip( + reversed(messages), reversed(senders)): # Get the name of the sender if any name = sender.first_name if sender else '???' @@ -147,13 +153,15 @@ class InteractiveTelegramClient(TelegramClient): if msg.media: self.found_media.add(msg) content = '<{}> {}'.format( # The media may or may not have a caption - msg.media.__class__.__name__, getattr(msg.media, 'caption', '')) + msg.media.__class__.__name__, + getattr(msg.media, 'caption', '')) else: content = msg.message # And print it to the user print('[{}:{}] (ID={}) {}: {}'.format( - msg.date.hour, msg.date.minute, msg.id, name, content)) + msg.date.hour, msg.date.minute, msg.id, name, + content)) # Send photo elif msg.startswith('!up '): @@ -176,18 +184,21 @@ class InteractiveTelegramClient(TelegramClient): print('Downloading profile picture...') success = self.download_profile_photo(entity.photo, output) if success: - print('Profile picture downloaded to {}'.format(output)) + print('Profile picture downloaded to {}'.format( + output)) else: print('"{}" does not seem to have a profile picture.' .format(get_display_name(entity))) # Send chat message (if any) elif msg: - self.send_message(input_peer, msg, markdown=True, no_web_page=True) + self.send_message( + input_peer, msg, markdown=True, no_web_page=True) def send_photo(self, path, peer): print('Uploading {}...'.format(path)) - input_file = self.upload_file(path, progress_callback=self.upload_progress_callback) + input_file = self.upload_file( + path, progress_callback=self.upload_progress_callback) # After we have the handle to the uploaded file, send it to our peer self.send_photo_file(input_file, peer) @@ -195,7 +206,8 @@ class InteractiveTelegramClient(TelegramClient): def send_document(self, path, peer): print('Uploading {}...'.format(path)) - input_file = self.upload_file(path, progress_callback=self.upload_progress_callback) + input_file = self.upload_file( + path, progress_callback=self.upload_progress_callback) # After we have the handle to the uploaded file, send it to our peer self.send_document_file(input_file, peer) @@ -212,9 +224,10 @@ class InteractiveTelegramClient(TelegramClient): # Let the output be the message ID output = str('usermedia/{}'.format(msg_media_id)) print('Downloading media with name {}...'.format(output)) - output = self.download_msg_media(msg.media, - file_path=output, - progress_callback=self.download_progress_callback) + output = self.download_msg_media( + msg.media, + file_path=output, + progress_callback=self.download_progress_callback) print('Media downloaded to {}!'.format(output)) except ValueError: @@ -222,32 +235,35 @@ class InteractiveTelegramClient(TelegramClient): @staticmethod def download_progress_callback(downloaded_bytes, total_bytes): - InteractiveTelegramClient.print_progress('Downloaded', downloaded_bytes, total_bytes) + InteractiveTelegramClient.print_progress('Downloaded', + downloaded_bytes, total_bytes) @staticmethod def upload_progress_callback(uploaded_bytes, total_bytes): - InteractiveTelegramClient.print_progress('Uploaded', uploaded_bytes, total_bytes) + InteractiveTelegramClient.print_progress('Uploaded', uploaded_bytes, + total_bytes) @staticmethod def print_progress(progress_type, downloaded_bytes, total_bytes): - print('{} {} out of {} ({:.2%})'.format( - progress_type, - bytes_to_string(downloaded_bytes), - bytes_to_string(total_bytes), - downloaded_bytes / total_bytes)) + print('{} {} out of {} ({:.2%})'.format(progress_type, bytes_to_string( + downloaded_bytes), bytes_to_string(total_bytes), downloaded_bytes / + total_bytes)) @staticmethod def update_handler(update_object): if type(update_object) is UpdateShortMessage: if update_object.out: - print('You sent {} to user #{}'.format(update_object.message, update_object.user_id)) + print('You sent {} to user #{}'.format(update_object.message, + update_object.user_id)) else: - print('[User #{} sent {}]'.format(update_object.user_id, update_object.message)) + print('[User #{} sent {}]'.format(update_object.user_id, + update_object.message)) elif type(update_object) is UpdateShortChatMessage: if update_object.out: - print('You sent {} to chat #{}'.format(update_object.message, update_object.chat_id)) + print('You sent {} to chat #{}'.format(update_object.message, + update_object.chat_id)) else: - print('[Chat #{}, user #{} sent {}]'.format(update_object.chat_id, - update_object.from_id, - update_object.message)) + print('[Chat #{}, user #{} sent {}]'.format( + update_object.chat_id, update_object.from_id, + update_object.message)) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index 49584331..60614581 100755 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -1,9 +1,10 @@ import os import time + import telethon.helpers as utils -from telethon.utils import BinaryWriter, BinaryReader -from telethon.crypto import AES, AuthKey, Factorizator, RSA +from telethon.crypto import AES, RSA, AuthKey, Factorizator from telethon.network import MtProtoPlainSender +from telethon.utils import BinaryReader, BinaryWriter def do_authentication(transport): @@ -23,7 +24,8 @@ def do_authentication(transport): with BinaryReader(sender.receive()) as reader: response_code = reader.read_int(signed=False) if response_code != 0x05162463: - raise AssertionError('Invalid response code: {}'.format(hex(response_code))) + raise AssertionError('Invalid response code: {}'.format( + hex(response_code))) nonce_from_server = reader.read(16) if nonce_from_server != nonce: @@ -36,7 +38,8 @@ def do_authentication(transport): vector_id = reader.read_int() if vector_id != 0x1cb5c415: - raise AssertionError('Invalid vector constructor ID: {}'.format(hex(response_code))) + raise AssertionError('Invalid vector constructor ID: {}'.format( + hex(response_code))) fingerprints = [] fingerprint_count = reader.read_int() @@ -47,32 +50,46 @@ def do_authentication(transport): new_nonce = os.urandom(32) p, q = Factorizator.factorize(pq) with BinaryWriter() as pq_inner_data_writer: - pq_inner_data_writer.write_int(0x83c95aec, signed=False) # PQ Inner Data + pq_inner_data_writer.write_int( + 0x83c95aec, signed=False) # PQ Inner Data pq_inner_data_writer.tgwrite_bytes(get_byte_array(pq, signed=False)) - pq_inner_data_writer.tgwrite_bytes(get_byte_array(min(p, q), signed=False)) - pq_inner_data_writer.tgwrite_bytes(get_byte_array(max(p, q), signed=False)) + pq_inner_data_writer.tgwrite_bytes( + get_byte_array( + min(p, q), signed=False)) + pq_inner_data_writer.tgwrite_bytes( + get_byte_array( + max(p, q), signed=False)) pq_inner_data_writer.write(nonce) pq_inner_data_writer.write(server_nonce) pq_inner_data_writer.write(new_nonce) cipher_text, target_fingerprint = None, None for fingerprint in fingerprints: - cipher_text = RSA.encrypt(get_fingerprint_text(fingerprint), pq_inner_data_writer.get_bytes()) + cipher_text = RSA.encrypt( + get_fingerprint_text(fingerprint), + pq_inner_data_writer.get_bytes()) if cipher_text is not None: target_fingerprint = fingerprint break if cipher_text is None: - raise AssertionError('Could not find a valid key for fingerprints: {}' - .format(', '.join([get_fingerprint_text(f) for f in fingerprints]))) + raise AssertionError( + 'Could not find a valid key for fingerprints: {}' + .format(', '.join([get_fingerprint_text(f) + for f in fingerprints]))) with BinaryWriter() as req_dh_params_writer: - req_dh_params_writer.write_int(0xd712e4be, signed=False) # Req DH Params + req_dh_params_writer.write_int( + 0xd712e4be, signed=False) # Req DH Params req_dh_params_writer.write(nonce) req_dh_params_writer.write(server_nonce) - req_dh_params_writer.tgwrite_bytes(get_byte_array(min(p, q), signed=False)) - req_dh_params_writer.tgwrite_bytes(get_byte_array(max(p, q), signed=False)) + req_dh_params_writer.tgwrite_bytes( + get_byte_array( + min(p, q), signed=False)) + req_dh_params_writer.tgwrite_bytes( + get_byte_array( + max(p, q), signed=False)) req_dh_params_writer.write(target_fingerprint) req_dh_params_writer.tgwrite_bytes(cipher_text) @@ -88,7 +105,8 @@ def do_authentication(transport): raise AssertionError('Server DH params fail: TODO') if response_code != 0xd0e8075c: - raise AssertionError('Invalid response code: {}'.format(hex(response_code))) + raise AssertionError('Invalid response code: {}'.format( + hex(response_code))) nonce_from_server = reader.read(16) if nonce_from_server != nonce: @@ -106,7 +124,6 @@ def do_authentication(transport): g, dh_prime, ga, time_offset = None, None, None, None with BinaryReader(plain_text_answer) as dh_inner_data_reader: - hashsum = dh_inner_data_reader.read(20) code = dh_inner_data_reader.read_int(signed=False) if code != 0xb5890dba: raise AssertionError('Invalid DH Inner Data code: {}'.format(code)) @@ -132,26 +149,34 @@ def do_authentication(transport): # Prepare client DH Inner Data with BinaryWriter() as client_dh_inner_data_writer: - client_dh_inner_data_writer.write_int(0x6643b654, signed=False) # Client DH Inner Data + client_dh_inner_data_writer.write_int( + 0x6643b654, signed=False) # Client DH Inner Data client_dh_inner_data_writer.write(nonce) client_dh_inner_data_writer.write(server_nonce) client_dh_inner_data_writer.write_long(0) # TODO retry_id - client_dh_inner_data_writer.tgwrite_bytes(get_byte_array(gb, signed=False)) + client_dh_inner_data_writer.tgwrite_bytes( + get_byte_array( + gb, signed=False)) with BinaryWriter() as client_dh_inner_data_with_hash_writer: - client_dh_inner_data_with_hash_writer.write(utils.sha1(client_dh_inner_data_writer.get_bytes())) - client_dh_inner_data_with_hash_writer.write(client_dh_inner_data_writer.get_bytes()) - client_dh_inner_data_bytes = client_dh_inner_data_with_hash_writer.get_bytes() + client_dh_inner_data_with_hash_writer.write( + utils.sha1(client_dh_inner_data_writer.get_bytes())) + client_dh_inner_data_with_hash_writer.write( + client_dh_inner_data_writer.get_bytes()) + client_dh_inner_data_bytes = client_dh_inner_data_with_hash_writer.get_bytes( + ) # Encryption - client_dh_inner_data_encrypted_bytes = AES.encrypt_ige(client_dh_inner_data_bytes, key, iv) + client_dh_inner_data_encrypted_bytes = AES.encrypt_ige( + client_dh_inner_data_bytes, key, iv) # Prepare Set client DH params with BinaryWriter() as set_client_dh_params_writer: set_client_dh_params_writer.write_int(0xf5045f1f, signed=False) set_client_dh_params_writer.write(nonce) set_client_dh_params_writer.write(server_nonce) - set_client_dh_params_writer.tgwrite_bytes(client_dh_inner_data_encrypted_bytes) + set_client_dh_params_writer.tgwrite_bytes( + client_dh_inner_data_encrypted_bytes) set_client_dh_params_bytes = set_client_dh_params_writer.get_bytes() sender.send(set_client_dh_params_bytes) @@ -171,7 +196,8 @@ def do_authentication(transport): new_nonce_hash1 = reader.read(16) auth_key = AuthKey(get_byte_array(gab, signed=False)) - new_nonce_hash_calculated = auth_key.calc_new_nonce_hash(new_nonce, 1) + new_nonce_hash_calculated = auth_key.calc_new_nonce_hash(new_nonce, + 1) if new_nonce_hash1 != new_nonce_hash_calculated: raise AssertionError('Invalid new nonce hash') @@ -200,7 +226,8 @@ def get_byte_array(integer, signed): """Gets the arbitrary-length byte array corresponding to the given integer""" bits = integer.bit_length() byte_length = (bits + 8 - 1) // 8 # 8 bits per byte - return int.to_bytes(integer, length=byte_length, byteorder='big', signed=signed) + return int.to_bytes( + integer, length=byte_length, byteorder='big', signed=signed) def get_int(byte_array, signed=True): diff --git a/telethon/network/mtproto_plain_sender.py b/telethon/network/mtproto_plain_sender.py index c7cb581d..094b0075 100755 --- a/telethon/network/mtproto_plain_sender.py +++ b/telethon/network/mtproto_plain_sender.py @@ -1,10 +1,12 @@ -import time import random -from telethon.utils import BinaryWriter, BinaryReader +import time + +from telethon.utils import BinaryReader, BinaryWriter 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 @@ -37,9 +39,12 @@ class MtProtoPlainSender: """Generates a new message ID based on the current time (in ms) since epoch""" # See https://core.telegram.org/mtproto/description#message-identifier-msg-id ms_time = int(time.time() * 1000) - new_msg_id = (((ms_time // 1000) << 32) | # "must approximately equal unixtime*2^32" - ((ms_time % 1000) << 22) | # "approximate moment in time the message was created" - random.randint(0, 524288) << 2) # "message identifiers are divisible by 4" + new_msg_id = (((ms_time // 1000) << 32) + | # "must approximately equal unixtime*2^32" + ((ms_time % 1000) << 22) + | # "approximate moment in time the message was created" + random.randint(0, 524288) + << 2) # "message identifiers are divisible by 4" # Ensure that we always return a message ID which is higher than the previous one if self._last_msg_id >= new_msg_id: diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 309fe5bf..65c84257 100755 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -1,18 +1,19 @@ import gzip -from telethon.errors import * -from time import sleep from datetime import timedelta -from threading import Thread, RLock +from threading import RLock, Thread +from time import sleep import telethon.helpers as utils from telethon.crypto import AES -from telethon.utils import BinaryWriter, BinaryReader -from telethon.tl.types import MsgsAck +from telethon.errors import * from telethon.tl.all_tlobjects import tlobjects +from telethon.tl.types import MsgsAck +from telethon.utils import BinaryReader, BinaryWriter class MtProtoSender: """MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)""" + def __init__(self, transport, session): self.transport = transport self.session = session @@ -27,7 +28,8 @@ class MtProtoSender: # We need this to avoid using the updates thread if we're waiting to read self.waiting_receive = False - self.updates_thread = Thread(target=self.updates_thread_method, name='Updates thread') + self.updates_thread = Thread( + target=self.updates_thread_method, name='Updates thread') self.updates_thread_running = False self.updates_thread_receiving = False @@ -118,7 +120,8 @@ class MtProtoSender: message, remote_msg_id, remote_sequence = self.decode_msg(body) with BinaryReader(message) as reader: - self.process_msg(remote_msg_id, remote_sequence, reader, request) + self.process_msg(remote_msg_id, remote_sequence, reader, + request) # We can now set the flag to False thus resuming the updates thread self.waiting_receive = False @@ -148,7 +151,8 @@ class MtProtoSender: # And then finally send the encrypted packet with BinaryWriter() as cipher_writer: - cipher_writer.write_long(self.session.auth_key.key_id, signed=False) + cipher_writer.write_long( + self.session.auth_key.key_id, signed=False) cipher_writer.write(msg_key) cipher_writer.write(cipher_text) self.transport.send(cipher_writer.get_bytes()) @@ -168,7 +172,8 @@ class MtProtoSender: msg_key = reader.read(16) key, iv = utils.calc_key(self.session.auth_key.key, msg_key, False) - plain_text = AES.decrypt_ige(reader.read(len(body) - reader.tell_position()), key, iv) + plain_text = AES.decrypt_ige( + reader.read(len(body) - reader.tell_position()), key, iv) with BinaryReader(plain_text) as plain_text_reader: remote_salt = plain_text_reader.read_long() @@ -198,7 +203,8 @@ class MtProtoSender: if code == 0x3072cfa1: # gzip_packed return self.handle_gzip_packed(msg_id, sequence, reader, request) if code == 0xedab447b: # bad_server_salt - return self.handle_bad_server_salt(msg_id, sequence, reader, request) + return self.handle_bad_server_salt(msg_id, sequence, reader, + request) if code == 0xa7eff811: # bad_msg_notification return self.handle_bad_msg_notification(msg_id, sequence, reader) @@ -253,7 +259,8 @@ class MtProtoSender: self.session.salt = new_salt if request is None: - raise ValueError('Tried to handle a bad server salt with no request specified') + raise ValueError( + 'Tried to handle a bad server salt with no request specified') # Resend self.send(request) @@ -277,15 +284,18 @@ class MtProtoSender: request.confirm_received = True if inner_code == 0x2144ca19: # RPC Error - error = RPCError(code=reader.read_int(), message=reader.tgread_string()) + error = RPCError( + code=reader.read_int(), message=reader.tgread_string()) if error.must_resend: if not request: - raise ValueError('The previously sent request must be resent. ' - 'However, no request was previously sent (called from updates thread).') + raise ValueError( + 'The previously sent request must be resent. ' + 'However, no request was previously sent (called from updates thread).') request.confirm_received = False if error.message.startswith('FLOOD_WAIT_'): - print('Should wait {}s. Sleeping until then.'.format(error.additional_data)) + print('Should wait {}s. Sleeping until then.'.format( + error.additional_data)) sleep(error.additional_data) elif error.message.startswith('PHONE_MIGRATE_'): @@ -295,7 +305,8 @@ class MtProtoSender: raise error else: if not request: - raise ValueError('Cannot receive a request from inside an RPC result from the updates thread.') + raise ValueError( + 'Cannot receive a request from inside an RPC result from the updates thread.') if inner_code == 0x3072cfa1: # GZip packed unpacked_data = gzip.decompress(reader.tgread_bytes()) @@ -311,7 +322,8 @@ class MtProtoSender: unpacked_data = gzip.decompress(packed_data) with BinaryReader(unpacked_data) as compressed_reader: - return self.process_msg(msg_id, sequence, compressed_reader, request) + return self.process_msg(msg_id, sequence, compressed_reader, + request) # endregion @@ -340,10 +352,12 @@ class MtProtoSender: try: self.updates_thread_receiving = True seq, body = self.transport.receive(timeout) - message, remote_msg_id, remote_sequence = self.decode_msg(body) + message, remote_msg_id, remote_sequence = self.decode_msg( + body) with BinaryReader(message) as reader: - self.process_msg(remote_msg_id, remote_sequence, reader) + self.process_msg(remote_msg_id, remote_sequence, + reader) except (ReadCancelledError, TimeoutError): pass diff --git a/telethon/network/tcp_client.py b/telethon/network/tcp_client.py index 6343c840..d8502c3d 100755 --- a/telethon/network/tcp_client.py +++ b/telethon/network/tcp_client.py @@ -78,7 +78,8 @@ class TcpClient: if timeout: time_passed = datetime.now() - start_time if time_passed > timeout: - raise TimeoutError('The read operation exceeded the timeout.') + raise TimeoutError( + 'The read operation exceeded the timeout.') # If everything went fine, return the read bytes return writer.get_bytes() diff --git a/telethon/network/tcp_transport.py b/telethon/network/tcp_transport.py index ec475270..ede0c151 100755 --- a/telethon/network/tcp_transport.py +++ b/telethon/network/tcp_transport.py @@ -1,8 +1,8 @@ from binascii import crc32 from datetime import timedelta -from telethon.network import TcpClient from telethon.errors import * +from telethon.network import TcpClient from telethon.utils import BinaryWriter @@ -27,7 +27,7 @@ class TcpTransport: crc = crc32(writer.get_bytes()) writer.write_int(crc, signed=False) - + self.send_counter += 1 self.tcp_client.write(writer.get_bytes()) @@ -45,9 +45,8 @@ class TcpTransport: body = self.tcp_client.read(packet_length - 12, timeout) - checksum = int.from_bytes(self.tcp_client.read(4, timeout), - byteorder='little', - signed=False) + checksum = int.from_bytes( + self.tcp_client.read(4, timeout), byteorder='little', signed=False) # Then perform the checks rv = packet_length_bytes + seq_bytes + body diff --git a/telethon/parser/markdown_parser.py b/telethon/parser/markdown_parser.py index 40b6cc07..59bc913b 100644 --- a/telethon/parser/markdown_parser.py +++ b/telethon/parser/markdown_parser.py @@ -1,4 +1,5 @@ -from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityCode, MessageEntityTextUrl +from telethon.tl.types import (MessageEntityBold, MessageEntityCode, + MessageEntityItalic, MessageEntityTextUrl) def parse_message_entities(msg): @@ -44,13 +45,13 @@ def parse_message_entities(msg): # Add 1 when slicing the message not to include the [] nor () # There is no need to subtract 1 on the later part because that index is already excluded - link_text = ''.join(msg[vui[0]+1:vui[1]]) - link_url = ''.join(msg[vui[2]+1:vui[3]]) + link_text = ''.join(msg[vui[0] + 1:vui[1]]) + link_url = ''.join(msg[vui[2] + 1:vui[3]]) # After we have retrieved both the link text and url, replace them in the message # Now we do have to add 1 to include the [] and () when deleting and replacing! - del msg[vui[2]:vui[3]+1] - msg[vui[0]:vui[1]+1] = link_text + del msg[vui[2]:vui[3] + 1] + msg[vui[0]:vui[1] + 1] = link_text # Finally, update the current valid index url to reflect that all the previous VUI's will be removed # This is because, after the previous VUI's get done, their part of the message is removed too, @@ -63,14 +64,12 @@ def parse_message_entities(msg): # No need to subtract the displacement from the URL part (indices 2 and 3) # When calculating the length, subtract 1 again not to include the previously called ']' - entities.append(MessageEntityTextUrl(offset=vui[0], length=vui[1] - vui[0] - 1, url=link_url)) + entities.append( + MessageEntityTextUrl( + offset=vui[0], length=vui[1] - vui[0] - 1, url=link_url)) # After the message is clean from links, handle all the indicator flags - indicator_flags = { - '*': None, - '_': None, - '`': None - } + indicator_flags = {'*': None, '_': None, '`': None} # Iterate over the list to find the indicators of entities for i, c in enumerate(msg): @@ -88,13 +87,19 @@ def parse_message_entities(msg): # Add the corresponding entity if c == '*': - entities.append(MessageEntityBold(offset=offset, length=length)) + entities.append( + MessageEntityBold( + offset=offset, length=length)) elif c == '_': - entities.append(MessageEntityItalic(offset=offset, length=length)) + entities.append( + MessageEntityItalic( + offset=offset, length=length)) elif c == '`': - entities.append(MessageEntityCode(offset=offset, length=length)) + entities.append( + MessageEntityCode( + offset=offset, length=length)) # Clear the flag to start over with this indicator indicator_flags[c] = None @@ -116,15 +121,16 @@ def parse_message_entities(msg): # In this case, the current entity length is decreased by two, # and all the subentities offset decreases 1 if (subentity.offset > entity.offset and - subentity.offset + subentity.length < entity.offset + entity.length): + subentity.offset + subentity.length < + entity.offset + entity.length): entity.length -= 2 subentity.offset -= 1 # Second case, both inside: so*me_th*in_g. # In this case, the current entity length is decreased by one, # and all the subentities offset and length decrease 1 - elif (entity.offset < subentity.offset < entity.offset + entity.length and - subentity.offset + subentity.length > entity.offset + entity.length): + elif (entity.offset < subentity.offset < entity.offset + + entity.length < subentity.offset + subentity.length): entity.length -= 1 subentity.offset -= 1 subentity.length -= 1 diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 0839c591..697cd6dd 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,47 +1,40 @@ import platform from datetime import datetime, timedelta from hashlib import md5 -from os import path, listdir from mimetypes import guess_type - -# For sending and receiving requests -from telethon.tl import MTProtoRequest -from telethon.tl import Session - -# The Requests and types that we'll be using -from telethon.tl.functions.upload import SaveBigFilePartRequest -from telethon.tl.functions import InvokeWithLayerRequest, InitConnectionRequest -from telethon.tl.functions.help import GetConfigRequest -from telethon.tl.functions.upload import SaveFilePartRequest, GetFileRequest -from telethon.tl.functions.messages import \ - GetDialogsRequest, GetHistoryRequest, \ - SendMessageRequest, SendMediaRequest, \ - ReadHistoryRequest - -from telethon.tl.functions.auth import \ - SendCodeRequest, CheckPasswordRequest, \ - SignInRequest, SignUpRequest, LogOutRequest - -# The following is required to get the password salt -from telethon.tl.functions.account import GetPasswordRequest - -# All the types we need to work with -from telethon.tl.types import \ - InputPeerEmpty, \ - UserProfilePhotoEmpty, ChatPhotoEmpty, \ - InputFile, InputFileLocation, InputMediaUploadedPhoto, InputMediaUploadedDocument, \ - MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, \ - DocumentAttributeAudio, DocumentAttributeFilename, InputDocumentFileLocation +from os import listdir, path # Import some externalized utilities to work with the Telegram types and more import telethon.helpers as utils import telethon.network.authenticator as authenticator -from telethon.utils import find_user_or_chat, get_appropiate_part_size, get_extension - from telethon.errors import * from telethon.network import MtProtoSender, TcpTransport from telethon.parser.markdown_parser import parse_message_entities +# For sending and receiving requests +from telethon.tl import MTProtoRequest, Session from telethon.tl.all_tlobjects import layer +from telethon.tl.functions import InitConnectionRequest, InvokeWithLayerRequest +# The following is required to get the password salt +from telethon.tl.functions.account import GetPasswordRequest +from telethon.tl.functions.auth import (CheckPasswordRequest, LogOutRequest, + SendCodeRequest, SignInRequest, + SignUpRequest) +from telethon.tl.functions.help import GetConfigRequest +from telethon.tl.functions.messages import ( + GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest, + SendMessageRequest) +# The Requests and types that we'll be using +from telethon.tl.functions.upload import ( + GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest) +# All the types we need to work with +from telethon.tl.types import ( + ChatPhotoEmpty, DocumentAttributeAudio, DocumentAttributeFilename, + InputDocumentFileLocation, InputFile, InputFileLocation, + InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty, + MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, + UserProfilePhotoEmpty) +from telethon.utils import (find_user_or_chat, get_appropiate_part_size, + get_extension) class TelegramClient: @@ -53,13 +46,15 @@ class TelegramClient: def __init__(self, session_user_id, api_id, api_hash): if api_id is None or api_hash is None: - raise PermissionError('Your API ID or Hash are invalid. Please read "Requirements" on README.rst') + raise PermissionError( + 'Your API ID or Hash are invalid. Please read "Requirements" on README.rst') self.api_id = api_id self.api_hash = api_hash self.session = Session.try_load_or_create_new(session_user_id) - self.transport = TcpTransport(self.session.server_address, self.session.port) + self.transport = TcpTransport(self.session.server_address, + self.session.port) # These will be set later self.dc_options = None @@ -88,14 +83,17 @@ class TelegramClient: # Now it's time to send an InitConnectionRequest # This must always be invoked with the layer we'll be using - query = InitConnectionRequest(api_id=self.api_id, - device_model=platform.node(), - system_version=platform.system(), - app_version=self.__version__, - lang_code='en', - query=GetConfigRequest()) + query = InitConnectionRequest( + api_id=self.api_id, + device_model=platform.node(), + system_version=platform.system(), + app_version=self.__version__, + lang_code='en', + query=GetConfigRequest()) - result = self.invoke(InvokeWithLayerRequest(layer=layer, query=query)) + result = self.invoke( + InvokeWithLayerRequest( + layer=layer, query=query)) # We're only interested in the DC options, # although many other options are available! @@ -114,7 +112,8 @@ class TelegramClient: def reconnect_to_dc(self, dc_id): """Reconnects to the specified DC ID. This is automatically called after an InvalidDCError is raised""" if self.dc_options is None or not self.dc_options: - raise ConnectionError("Can't reconnect. Stabilise an initial connection first.") + raise ConnectionError( + "Can't reconnect. Stabilise an initial connection first.") dc = next(dc for dc in self.dc_options if dc.id == dc_id) @@ -175,11 +174,13 @@ class TelegramClient: with `.password_required = True` was raised""" if phone_number and code: if phone_number not in self.phone_code_hashes: - raise ValueError('Please make sure you have called send_code_request first.') + raise ValueError( + 'Please make sure you have called send_code_request first.') try: - result = self.invoke(SignInRequest( - phone_number, self.phone_code_hashes[phone_number], code)) + result = self.invoke( + SignInRequest(phone_number, self.phone_code_hashes[ + phone_number], code)) except RPCError as error: if error.message.startswith('PHONE_CODE_'): @@ -189,10 +190,12 @@ class TelegramClient: raise error elif password: salt = self.invoke(GetPasswordRequest()).current_salt - result = self.invoke(CheckPasswordRequest(utils.get_password_hash(password, salt))) + result = self.invoke( + CheckPasswordRequest(utils.get_password_hash(password, salt))) else: - raise ValueError('You must provide a phone_number and a code for the first time, ' - 'and a password only if an RPCError was raised before.') + raise ValueError( + 'You must provide a phone_number and a code for the first time, ' + 'and a password only if an RPCError was raised before.') # Result is an Auth.Authorization TLObject self.session.user = result.user @@ -205,11 +208,13 @@ class TelegramClient: def sign_up(self, phone_number, code, first_name, last_name=''): """Signs up to Telegram. Make sure you sent a code request first!""" - result = self.invoke(SignUpRequest(phone_number=phone_number, - phone_code_hash=self.phone_code_hashes[phone_number], - phone_code=code, - first_name=first_name, - last_name=last_name)) + result = self.invoke( + SignUpRequest( + phone_number=phone_number, + phone_code_hash=self.phone_code_hashes[phone_number], + phone_code=code, + first_name=first_name, + last_name=last_name)) self.session.user = result.user self.session.save() @@ -229,29 +234,41 @@ class TelegramClient: def list_sessions(): """Lists all the sessions of the users who have ever connected using this client and never logged out""" - return [path.splitext(path.basename(f))[0] # splitext = split ext (not spli text!) + return [path.splitext(path.basename(f))[ + 0] # splitext = split ext (not spli text!) for f in listdir('.') if f.endswith('.session')] # endregion # region Dialogs ("chats") requests - def get_dialogs(self, count=10, offset_date=None, offset_id=0, offset_peer=InputPeerEmpty()): + def get_dialogs(self, + count=10, + offset_date=None, + offset_id=0, + offset_peer=InputPeerEmpty()): """Returns a tuple of lists ([dialogs], [entities]) with 'count' items each. The `entity` represents the user, chat or channel corresponding to that dialog""" - r = self.invoke(GetDialogsRequest(offset_date=offset_date, - offset_id=offset_id, - offset_peer=offset_peer, - limit=count)) - return (r.dialogs, - [find_user_or_chat(d.peer, r.users, r.chats) for d in r.dialogs]) + r = self.invoke( + GetDialogsRequest( + offset_date=offset_date, + offset_id=offset_id, + offset_peer=offset_peer, + limit=count)) + return ( + r.dialogs, + [find_user_or_chat(d.peer, r.users, r.chats) for d in r.dialogs]) # endregion # region Message requests - def send_message(self, input_peer, message, markdown=False, no_web_page=False): + def send_message(self, + input_peer, + message, + markdown=False, + no_web_page=False): """Sends a message to the given input peer and returns the sent message ID""" if markdown: msg, entities = parse_message_entities(message) @@ -259,15 +276,23 @@ class TelegramClient: msg, entities = message, [] msg_id = utils.generate_random_long() - self.invoke(SendMessageRequest(peer=input_peer, - message=msg, - random_id=msg_id, - entities=entities, - no_webpage=no_web_page)) + self.invoke( + SendMessageRequest( + peer=input_peer, + message=msg, + random_id=msg_id, + entities=entities, + no_webpage=no_web_page)) return msg_id - def get_message_history(self, input_peer, limit=20, - offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0): + def get_message_history(self, + input_peer, + limit=20, + offset_date=None, + offset_id=0, + max_id=0, + min_id=0, + add_offset=0): """ Gets the message history for the specified InputPeer @@ -282,13 +307,15 @@ class TelegramClient: :return: A tuple containing total message count and two more lists ([messages], [senders]). Note that the sender can be null if it was not found! """ - result = self.invoke(GetHistoryRequest(input_peer, - limit=limit, - offset_date=offset_date, - offset_id=offset_id, - max_id=max_id, - min_id=min_id, - add_offset=add_offset)) + result = self.invoke( + GetHistoryRequest( + input_peer, + limit=limit, + offset_date=offset_date, + offset_id=offset_id, + max_id=max_id, + min_id=min_id, + add_offset=add_offset)) # The result may be a messages slice (not all messages were retrieved) or # simply a messages TLObject. In the later case, no "count" attribute is specified: @@ -315,7 +342,8 @@ class TelegramClient: Returns an AffectedMessages TLObject""" if max_id is None: if not messages: - raise InvalidParameterError('Either a message list or a max_id must be provided.') + raise InvalidParameterError( + 'Either a message list or a max_id must be provided.') if isinstance(messages, list): max_id = max(msg.id for msg in messages) @@ -331,7 +359,11 @@ class TelegramClient: # be handled through a separate session and a separate connection" # region Uploading media requests - def upload_file(self, file_path, part_size_kb=None, file_name=None, progress_callback=None): + def upload_file(self, + file_path, + part_size_kb=None, + file_name=None, + progress_callback=None): """Uploads the specified file_path and returns a handle which can be later used :param file_path: The file path of the file that will be uploaded @@ -359,7 +391,7 @@ class TelegramClient: # Multiply the datetime timestamp by 10^6 to get the ticks # This is high likely going to be unique - file_id = int(datetime.now().timestamp() * (10 ** 6)) + file_id = int(datetime.now().timestamp() * (10**6)) hash_md5 = md5() with open(file_path, 'rb') as file: @@ -370,7 +402,8 @@ class TelegramClient: # The SavePartRequest is different depending on whether # the file is too large or not (over or less than 10MB) if is_large: - request = SaveBigFilePartRequest(file_id, part_index, part_count, part) + request = SaveBigFilePartRequest(file_id, part_index, + part_count, part) else: request = SaveFilePartRequest(file_id, part_index, part) @@ -381,17 +414,19 @@ class TelegramClient: if progress_callback: progress_callback(file.tell(), file_size) else: - raise ValueError('Could not upload file part #{}'.format(part_index)) + raise ValueError('Could not upload file part #{}'.format( + part_index)) # Set a default file name if None was specified if not file_name: file_name = path.basename(file_path) # After the file has been uploaded, we can return a handle pointing to it - return InputFile(id=file_id, - parts=part_count, - name=file_name, - md5_checksum=hash_md5.hexdigest()) + return InputFile( + id=file_id, + parts=part_count, + name=file_name, + md5_checksum=hash_md5.hexdigest()) def send_photo_file(self, input_file, input_peer, caption=''): """Sends a previously uploaded input_file @@ -415,28 +450,36 @@ class TelegramClient: # «The "octet-stream" subtype is used to indicate that a body contains arbitrary binary data.» if not mime_type: mime_type = 'application/octet-stream' - self.send_media_file(InputMediaUploadedDocument(file=input_file, - mime_type=mime_type, - attributes=attributes, - caption=caption), input_peer) + self.send_media_file( + InputMediaUploadedDocument( + file=input_file, + mime_type=mime_type, + attributes=attributes, + caption=caption), + input_peer) def send_media_file(self, input_media, input_peer): """Sends any input_media (contact, document, photo...) to an input_peer""" - self.invoke(SendMediaRequest(peer=input_peer, - media=input_media, - random_id=utils.generate_random_long())) + self.invoke( + SendMediaRequest( + peer=input_peer, + media=input_media, + random_id=utils.generate_random_long())) # endregion # region Downloading media requests - def download_profile_photo(self, profile_photo, file_path, - add_extension=True, download_big=True): + def download_profile_photo(self, + profile_photo, + file_path, + add_extension=True, + download_big=True): """Downloads the profile photo for an user or a chat (including channels). Returns False if no photo was providen, or if it was Empty""" if (not profile_photo or - isinstance(profile_photo, UserProfilePhotoEmpty) or + isinstance(profile_photo, UserProfilePhotoEmpty) or isinstance(profile_photo, ChatPhotoEmpty)): return False @@ -449,28 +492,40 @@ class TelegramClient: photo_location = profile_photo.photo_small # Download the media with the largest size input file location - self.download_file_loc(InputFileLocation(volume_id=photo_location.volume_id, - local_id=photo_location.local_id, - secret=photo_location.secret), - file_path) + self.download_file_loc( + InputFileLocation( + volume_id=photo_location.volume_id, + local_id=photo_location.local_id, + secret=photo_location.secret), + file_path) return True - def download_msg_media(self, message_media, file_path, add_extension=True, progress_callback=None): + def download_msg_media(self, + message_media, + file_path, + add_extension=True, + progress_callback=None): """Downloads the given MessageMedia (Photo, Document or Contact) into the desired file_path, optionally finding its extension automatically The progress_callback should be a callback function which takes two parameters, uploaded size (in bytes) and total file size (in bytes). This will be called every time a part is downloaded""" if type(message_media) == MessageMediaPhoto: - return self.download_photo(message_media, file_path, add_extension, progress_callback) + return self.download_photo(message_media, file_path, add_extension, + progress_callback) elif type(message_media) == MessageMediaDocument: - return self.download_document(message_media, file_path, add_extension, progress_callback) + return self.download_document(message_media, file_path, + add_extension, progress_callback) elif type(message_media) == MessageMediaContact: - return self.download_contact(message_media, file_path, add_extension) + return self.download_contact(message_media, file_path, + add_extension) - def download_photo(self, message_media_photo, file_path, add_extension=False, + def download_photo(self, + message_media_photo, + file_path, + add_extension=False, progress_callback=None): """Downloads MessageMediaPhoto's largest size into the desired file_path, optionally finding its extension automatically @@ -488,13 +543,20 @@ class TelegramClient: file_path += get_extension(message_media_photo) # Download the media with the largest size input file location - self.download_file_loc(InputFileLocation(volume_id=largest_size.volume_id, - local_id=largest_size.local_id, - secret=largest_size.secret), - file_path, file_size=file_size, progress_callback=progress_callback) + self.download_file_loc( + InputFileLocation( + volume_id=largest_size.volume_id, + local_id=largest_size.local_id, + secret=largest_size.secret), + file_path, + file_size=file_size, + progress_callback=progress_callback) return file_path - def download_document(self, message_media_document, file_path=None, add_extension=True, + def download_document(self, + message_media_document, + file_path=None, + add_extension=True, progress_callback=None): """Downloads the given MessageMediaDocument into the desired file_path, optionally finding its extension automatically. @@ -521,10 +583,14 @@ class TelegramClient: if add_extension: file_path += get_extension(document.mime_type) - self.download_file_loc(InputDocumentFileLocation(id=document.id, - access_hash=document.access_hash, - version=document.version), - file_path, file_size=file_size, progress_callback=progress_callback) + self.download_file_loc( + InputDocumentFileLocation( + id=document.id, + access_hash=document.access_hash, + version=document.version), + file_path, + file_size=file_size, + progress_callback=progress_callback) return file_path @staticmethod @@ -546,15 +612,21 @@ class TelegramClient: with open(file_path, 'w', encoding='utf-8') as file: file.write('BEGIN:VCARD\n') file.write('VERSION:4.0\n') - file.write('N:{};{};;;\n'.format(first_name, last_name if last_name else '')) + file.write('N:{};{};;;\n'.format(first_name, last_name + if last_name else '')) file.write('FN:{}\n'.format(' '.join((first_name, last_name)))) - file.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(phone_number)) + file.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format( + phone_number)) file.write('END:VCARD\n') return file_path - def download_file_loc(self, input_location, file_path, part_size_kb=64, - file_size=None, progress_callback=None): + def download_file_loc(self, + input_location, + file_path, + part_size_kb=64, + file_size=None, + progress_callback=None): """Downloads media from the given input_file_location to the specified file_path. If a progress_callback function is given, it will be called taking two arguments (downloaded bytes count and total file size)""" @@ -578,7 +650,8 @@ class TelegramClient: while True: # The current offset equals the offset_index multiplied by the part size offset = offset_index * part_size - result = self.invoke(GetFileRequest(input_location, offset, part_size)) + result = self.invoke( + GetFileRequest(input_location, offset, part_size)) offset_index += 1 # If we have received no data (0 bytes), the file is over @@ -600,7 +673,8 @@ class TelegramClient: """Adds an update handler (a function which takes a TLObject, an update, as its parameter) and listens for updates""" if not self.signed_in: - raise ValueError("You cannot add update handlers until you've signed in.") + raise ValueError( + "You cannot add update handlers until you've signed in.") self.sender.add_update_handler(handler) diff --git a/telethon/tl/mtproto_request.py b/telethon/tl/mtproto_request.py index 4f7d58db..3c319d8c 100755 --- a/telethon/tl/mtproto_request.py +++ b/telethon/tl/mtproto_request.py @@ -26,8 +26,9 @@ class MTProtoRequest: self.confirm_received = True def need_resend(self): - return self.dirty or (self.confirmed and not self.confirm_received and - datetime.now() - self.send_time > timedelta(seconds=3)) + return self.dirty or ( + self.confirmed and not self.confirm_received and + datetime.now() - self.send_time > timedelta(seconds=3)) # These should be overrode def on_send(self, writer): diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 088b224a..d2873889 100755 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -1,8 +1,9 @@ -from os.path import isfile as file_exists import os -import time import pickle import random +import time +from os.path import isfile as file_exists + import telethon.helpers as utils @@ -39,12 +40,11 @@ class Session: If the given session_user_id is None, we assume that it is for testing purposes""" if session_user_id is None: return Session(None) - else: - filepath = '{}.session'.format(session_user_id) + path = '{}.session'.format(session_user_id) - if file_exists(filepath): - with open(filepath, 'rb') as file: + if file_exists(path): + with open(path, 'rb') as file: return pickle.load(file) else: return Session(session_user_id) @@ -53,9 +53,12 @@ class Session: """Generates a new message ID based on the current time (in ms) since epoch""" # Refer to mtproto_plain_sender.py for the original method, this is a simple copy ms_time = int(time.time() * 1000) - new_msg_id = (((ms_time // 1000 + self.time_offset) << 32) | # "must approximately equal unixtime*2^32" - ((ms_time % 1000) << 22) | # "approximate moment in time the message was created" - random.randint(0, 524288) << 2) # "message identifiers are divisible by 4" + new_msg_id = (((ms_time // 1000 + self.time_offset) << 32) + | # "must approximately equal unixtime*2^32" + ((ms_time % 1000) << 22) + | # "approximate moment in time the message was created" + random.randint(0, 524288) + << 2) # "message identifiers are divisible by 4" if self.last_message_id >= new_msg_id: new_msg_id = self.last_message_id + 4 diff --git a/telethon/utils/binary_reader.py b/telethon/utils/binary_reader.py index 6d04671e..fcbefea3 100755 --- a/telethon/utils/binary_reader.py +++ b/telethon/utils/binary_reader.py @@ -1,10 +1,10 @@ -from datetime import datetime -from io import BytesIO, BufferedReader -from telethon.tl.all_tlobjects import tlobjects -from struct import unpack -from telethon.errors import * -import inspect import os +from datetime import datetime +from io import BufferedReader, BytesIO +from struct import unpack + +from telethon.errors import * +from telethon.tl.all_tlobjects import tlobjects class BinaryReader: @@ -12,13 +12,15 @@ class BinaryReader: Small utility class to read binary data. Also creates a "Memory Stream" if necessary """ + def __init__(self, data=None, stream=None): if data: self.stream = BytesIO(data) elif stream: self.stream = stream else: - raise InvalidParameterError("Either bytes or a stream must be provided") + raise InvalidParameterError( + 'Either bytes or a stream must be provided') self.reader = BufferedReader(self.stream) @@ -47,14 +49,16 @@ class BinaryReader: def read_large_int(self, bits, signed=True): """Reads a n-bits long integer value""" - return int.from_bytes(self.read(bits // 8), byteorder='little', signed=signed) + return int.from_bytes( + self.read(bits // 8), byteorder='little', signed=signed) def read(self, length): """Read the given amount of bytes""" result = self.reader.read(length) if len(result) != length: - raise BufferError('Trying to read outside the data bounds (no more data left to read)') - + raise BufferError( + 'Trying to read outside the data bounds (no more data left to read)') + return result def get_bytes(self): @@ -69,7 +73,8 @@ class BinaryReader: """Reads a Telegram-encoded byte array, without the need of specifying its length""" first_byte = self.read_byte() if first_byte == 254: - length = self.read_byte() | (self.read_byte() << 8) | (self.read_byte() << 16) + length = self.read_byte() | (self.read_byte() << 8) | ( + self.read_byte() << 16) padding = length % 4 else: length = first_byte diff --git a/telethon/utils/binary_writer.py b/telethon/utils/binary_writer.py index 026d139c..00f37e42 100755 --- a/telethon/utils/binary_writer.py +++ b/telethon/utils/binary_writer.py @@ -1,4 +1,4 @@ -from io import BytesIO, BufferedWriter +from io import BufferedWriter, BytesIO from struct import pack @@ -26,12 +26,16 @@ class BinaryWriter: def write_int(self, value, signed=True): """Writes an integer value (4 bytes), which can or cannot be signed""" - self.writer.write(int.to_bytes(value, length=4, byteorder='little', signed=signed)) + self.writer.write( + int.to_bytes( + value, length=4, byteorder='little', signed=signed)) self.written_count += 4 def write_long(self, value, signed=True): """Writes a long integer value (8 bytes), which can or cannot be signed""" - self.writer.write(int.to_bytes(value, length=8, byteorder='little', signed=signed)) + self.writer.write( + int.to_bytes( + value, length=8, byteorder='little', signed=signed)) self.written_count += 8 def write_float(self, value): @@ -46,7 +50,9 @@ class BinaryWriter: def write_large_int(self, value, bits, signed=True): """Writes a n-bits long integer value""" - self.writer.write(int.to_bytes(value, length=bits // 8, byteorder='little', signed=signed)) + self.writer.write( + int.to_bytes( + value, length=bits // 8, byteorder='little', signed=signed)) self.written_count += bits // 8 def write(self, data): diff --git a/telethon/utils/tl_utils.py b/telethon/utils/tl_utils.py index 71714f69..88cfffa5 100644 --- a/telethon/utils/tl_utils.py +++ b/telethon/utils/tl_utils.py @@ -4,12 +4,10 @@ after all, both are the same attribute, IDs.""" from mimetypes import add_type, guess_extension -from telethon.tl.types import \ - User, Chat, Channel, \ - PeerUser, PeerChat, PeerChannel, \ - InputPeerUser, InputPeerChat, InputPeerChannel, \ - UserProfilePhoto, ChatPhoto, \ - MessageMediaPhoto, MessageMediaDocument +from telethon.tl.types import ( + Channel, Chat, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, + MessageMediaDocument, MessageMediaPhoto, PeerChannel, PeerChat, PeerUser, + User, UserProfilePhoto) def get_display_name(entity): @@ -31,8 +29,7 @@ def get_extension(media): """Gets the corresponding extension for any Telegram media""" # Photos are always compressed as .jpg by Telegram - if (isinstance(media, UserProfilePhoto) or - isinstance(media, ChatPhoto) or + if (isinstance(media, UserProfilePhoto) or isinstance(media, ChatPhoto) or isinstance(media, MessageMediaPhoto)): return '.jpg' diff --git a/telethon_generator/parser/source_builder.py b/telethon_generator/parser/source_builder.py index 5ce24024..10ee028b 100755 --- a/telethon_generator/parser/source_builder.py +++ b/telethon_generator/parser/source_builder.py @@ -18,7 +18,8 @@ class SourceBuilder: """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 + if string.strip( + ): # If the string was not empty, indent; Else it probably was a new line self.indent() self.out_stream.write(string) diff --git a/telethon_generator/parser/tl_object.py b/telethon_generator/parser/tl_object.py index d7325731..5c225162 100755 --- a/telethon_generator/parser/tl_object.py +++ b/telethon_generator/parser/tl_object.py @@ -5,13 +5,13 @@ class TLObject: """.tl core types IDs (such as vector, booleans, etc.)""" CORE_TYPES = (0x1cb5c415, 0xbc799737, 0x997275b5, 0x3fedd339) - def __init__(self, fullname, id, args, result, is_function): + def __init__(self, fullname, object_id, args, result, is_function): """ Initializes a new TLObject, given its properties. Usually, this will be called from `from_tl` instead :param fullname: The fullname of the TL object (namespace.name) The namespace can be omitted - :param id: The hexadecimal string representing the object ID + :param object_id: The hexadecimal string representing the object ID :param args: The arguments, if any, of the TL object :param result: The result type of the TL object :param is_function: Is the object a function or a type? @@ -25,7 +25,7 @@ class TLObject: self.name = fullname # The ID should be an hexadecimal string - self.id = int(id, base=16) + self.id = int(object_id, base=16) self.args = args self.result = result self.is_function = is_function @@ -67,14 +67,16 @@ class TLObject: ''', tl, re.IGNORECASE | re.VERBOSE) # Retrieve the matched arguments - args = [TLArg(name, type, brace != '') for brace, name, type, _ in args_match] + args = [TLArg(name, arg_type, brace != '') + for brace, name, arg_type, _ in args_match] # And initialize the TLObject - return TLObject(fullname=match.group(1), - id=match.group(2), - args=args, - result=match.group(3), - is_function=is_function) + return TLObject( + fullname=match.group(1), + object_id=match.group(2), + args=args, + result=match.group(3), + is_function=is_function) def is_core_type(self): """Determines whether the TLObject is a "core type" @@ -82,19 +84,19 @@ class TLObject: return self.id in TLObject.CORE_TYPES def __repr__(self): - fullname = ('{}.{}'.format(self.namespace, self.name) if self.namespace is not None - else self.name) + fullname = ('{}.{}'.format(self.namespace, self.name) + if self.namespace is not None else self.name) - hex_id = hex(self.id)[2:].rjust(8, '0') # Skip 0x and add 0's for padding + hex_id = hex(self.id)[2:].rjust(8, + '0') # Skip 0x and add 0's for padding - return '{}#{} {} = {}'.format(fullname, - hex_id, - ' '.join([str(arg) for arg in self.args]), - self.result) + return '{}#{} {} = {}'.format( + fullname, hex_id, ' '.join([str(arg) for arg in self.args]), + self.result) def __str__(self): - fullname = ('{}.{}'.format(self.namespace, self.name) if self.namespace is not None - else self.name) + fullname = ('{}.{}'.format(self.namespace, self.name) + if self.namespace is not None else self.name) # Some arguments are not valid for being represented, such as the flag indicator or generic definition # (these have no explicit values until used) @@ -104,20 +106,21 @@ class TLObject: args = ', '.join(['{}={{}}'.format(arg.name) for arg in valid_args]) # Since Python's default representation for lists is using repr(), we need to str() manually on every item - args_format = ', '.join(['str(self.{})'.format(arg.name) if not arg.is_vector else - 'None if not self.{0} else [str(_) for _ in self.{0}]'.format(arg.name) - for arg in valid_args]) + args_format = ', '.join( + ['str(self.{})'.format(arg.name) if not arg.is_vector else + 'None if not self.{0} else [str(_) for _ in self.{0}]'.format( + arg.name) for arg in valid_args]) return ("'({} (ID: {}) = ({}))'.format({})" .format(fullname, hex(self.id), args, args_format)) class TLArg: - def __init__(self, name, type, generic_definition): + def __init__(self, name, arg_type, generic_definition): """ Initializes a new .tl argument :param name: The name of the .tl argument - :param type: The type of the .tl argument + :param arg_type: The type of the .tl argument :param generic_definition: Is the argument a generic definition? (i.e. {X:Type}) """ @@ -132,14 +135,15 @@ class TLArg: self.flag_index = -1 # The type can be an indicator that other arguments will be flags - if type == '#': + if arg_type == '#': self.flag_indicator = True self.type = None self.is_generic = False else: self.flag_indicator = False - self.is_generic = type.startswith('!') - self.type = type.lstrip('!') # Strip the exclamation mark always to have only the name + self.is_generic = arg_type.startswith('!') + self.type = arg_type.lstrip( + '!') # Strip the exclamation mark always to have only the name # The type may be a flag (flags.IDX?REAL_TYPE) # Note that «flags» is NOT the flags name; this is determined by a previous argument @@ -148,13 +152,15 @@ class TLArg: if flag_match: self.is_flag = True self.flag_index = int(flag_match.group(1)) - self.type = flag_match.group(2) # Update the type to match the exact type, not the "flagged" one + self.type = flag_match.group( + 2) # Update the type to match the exact type, not the "flagged" one # Then check if the type is a Vector vector_match = re.match(r'vector<(\w+)>', self.type, re.IGNORECASE) if vector_match: self.is_vector = True - self.type = vector_match.group(1) # Update the type to match the one inside the vector + self.type = vector_match.group( + 1) # Update the type to match the one inside the vector # The name may contain "date" in it, if this is the case and the type is "int", # we can safely assume that this should be treated as a "date" object. diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index a7fcd2bf..3d2eecdd 100755 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -2,7 +2,7 @@ import os import re import shutil -from parser import SourceBuilder, TLParser +from .parser import SourceBuilder, TLParser def get_output_path(normal_path): @@ -60,8 +60,8 @@ class TLGenerator: continue # Determine the output directory and create it - out_dir = get_output_path('functions' if tlobject.is_function - else 'types') + out_dir = get_output_path('functions' + if tlobject.is_function else 'types') if tlobject.namespace: out_dir = os.path.join(out_dir, tlobject.namespace) @@ -77,39 +77,51 @@ class TLGenerator: TLGenerator.get_class_name(tlobject))) # Create the file for this TLObject - filename = os.path.join(out_dir, TLGenerator.get_file_name(tlobject, add_extension=True)) + filename = os.path.join( + out_dir, + TLGenerator.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: # Both types and functions inherit from MTProtoRequest so they all can be sent - builder.writeln('from telethon.tl.mtproto_request import MTProtoRequest') + builder.writeln( + 'from telethon.tl.mtproto_request import MTProtoRequest') builder.writeln() builder.writeln() - builder.writeln('class {}(MTProtoRequest):'.format(TLGenerator.get_class_name(tlobject))) + builder.writeln('class {}(MTProtoRequest):'.format( + TLGenerator.get_class_name(tlobject))) # 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( + '"""Class generated by TLObjects\' generator. ' + 'All changes will be ERASED. Original .tl definition below.') builder.writeln('{}"""'.format(repr(tlobject))) builder.writeln() # Create an class-level variable that stores the TLObject's constructor ID - builder.writeln("# Telegram's constructor ID (and unique identifier) for this class") - builder.writeln('constructor_id = {}'.format(hex(tlobject.id))) + builder.writeln( + "# Telegram's constructor ID (and unique identifier) for this class") + builder.writeln('constructor_id = {}'.format( + hex(tlobject.id))) builder.writeln() # First sort the arguments so that those not being a flag come first - args = sorted([arg for arg in tlobject.args if not arg.flag_indicator], - key=lambda x: x.is_flag) + args = sorted( + [arg for arg in tlobject.args + if not arg.flag_indicator], + key=lambda x: x.is_flag) # Then convert the args to string parameters, the flags having =None - args = [(arg.name if not arg.is_flag - else '{}=None'.format(arg.name)) for arg in args - if not arg.flag_indicator and not arg.generic_definition] + args = [(arg.name if not arg.is_flag else + '{}=None'.format(arg.name)) for arg in args + if not arg.flag_indicator and + not arg.generic_definition] # Write the __init__ function if args: - builder.writeln('def __init__(self, {}):'.format(', '.join(args))) + builder.writeln('def __init__(self, {}):'.format( + ', '.join(args))) else: builder.writeln('def __init__(self):') @@ -117,18 +129,23 @@ class TLGenerator: # those which are generated automatically: flag indicator and generic definitions. # We don't need the generic definitions in Python because arguments can be any type args = [arg for arg in tlobject.args - if not arg.flag_indicator and not arg.generic_definition] + if not arg.flag_indicator and + not arg.generic_definition] if args: # Write the docstring, so we know the type of the arguments builder.writeln('"""') for arg in args: if not arg.flag_indicator: - builder.write(':param {}: Telegram type: «{}».'.format(arg.name, arg.type)) + builder.write( + ':param {}: Telegram type: «{}».'.format( + arg.name, arg.type)) if arg.is_vector: - builder.write(' Must be a list.'.format(arg.name)) + builder.write(' Must be a list.'.format( + arg.name)) if arg.is_generic: - builder.write(' This should be another MTProtoRequest.') + builder.write( + ' This should be another MTProtoRequest.') builder.writeln() builder.writeln('"""') @@ -136,7 +153,8 @@ class TLGenerator: # Functions have a result object and are confirmed by default if tlobject.is_function: builder.writeln('self.result = None') - builder.writeln('self.confirmed = True # Confirmed by default') + builder.writeln( + 'self.confirmed = True # Confirmed by default') # Set the arguments if args: @@ -148,22 +166,24 @@ class TLGenerator: # Write the on_send(self, writer) function builder.writeln('def on_send(self, writer):') - builder.writeln('writer.write_int({}.constructor_id, signed=False)' - .format(TLGenerator.get_class_name(tlobject))) + builder.writeln( + 'writer.write_int({}.constructor_id, signed=False)' + .format(TLGenerator.get_class_name(tlobject))) for arg in tlobject.args: - TLGenerator.write_onsend_code(builder, arg, tlobject.args) + TLGenerator.write_onsend_code(builder, arg, + tlobject.args) builder.end_block() # Write the empty() function, which returns an "empty" # instance, in which all attributes are set to None builder.writeln('@staticmethod') builder.writeln('def empty():') - builder.writeln('"""Returns an "empty" instance (all attributes are None)"""') + builder.writeln( + '"""Returns an "empty" instance (all attributes are None)"""') builder.writeln('return {}({})'.format( - TLGenerator.get_class_name(tlobject), - ', '.join('None' for _ in range(len(args))) - )) + TLGenerator.get_class_name(tlobject), ', '.join( + 'None' for _ in range(len(args))))) builder.end_block() # Write the on_response(self, reader) function @@ -174,7 +194,8 @@ class TLGenerator: else: if tlobject.args: for arg in tlobject.args: - TLGenerator.write_onresponse_code(builder, arg, tlobject.args) + TLGenerator.write_onresponse_code( + builder, arg, tlobject.args) else: # If there were no arguments, we still need an on_response method, and hence "pass" if empty builder.writeln('pass') @@ -186,23 +207,26 @@ class TLGenerator: builder.end_block() builder.writeln('def __str__(self):') - builder.writeln("return {}".format(str(tlobject))) + builder.writeln('return {}'.format(str(tlobject))) # builder.end_block() # There is no need to end the last block - # Step 3: Once all the objects have been generated, we can now group them in a single file + # Step 3: Once all the objects have been generated, we can now group them in a single file filename = os.path.join(get_output_path('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( + '"""File generated by TLObjects\' generator. All changes will be ERASED"""') builder.writeln() # First add imports for tlobject in tlobjects: - builder.writeln('import {}'.format(TLGenerator.get_full_file_name(tlobject))) + builder.writeln('import {}'.format( + TLGenerator.get_full_file_name(tlobject))) builder.writeln() # Create a variable to indicate which layer this is - builder.writeln('layer = {} # Current generated layer'.format(TLParser.find_layer(scheme_file))) + builder.writeln('layer = {} # Current generated layer'.format( + TLParser.find_layer(scheme_file))) builder.writeln() # Then create the dictionary containing constructor_id: class @@ -211,10 +235,9 @@ class TLGenerator: # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) for tlobject in tlobjects: - builder.writeln('{}: {}.{},' - .format(hex(tlobject.id), - TLGenerator.get_full_file_name(tlobject), - TLGenerator.get_class_name(tlobject))) + builder.writeln('{}: {}.{},'.format( + hex(tlobject.id), TLGenerator.get_full_file_name( + tlobject), TLGenerator.get_class_name(tlobject))) builder.current_indent -= 1 builder.writeln('}') @@ -225,8 +248,10 @@ class TLGenerator: # Courtesy of http://stackoverflow.com/a/31531797/4759433 # Also, '_' could be replaced for ' ', then use .title(), and then remove ' ' - result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), tlobject.name) - result = result[:1].upper() + result[1:].replace('_', '') # Replace again to fully ensure! + result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), + tlobject.name) + result = result[:1].upper() + result[1:].replace( + '_', '') # Replace again to fully ensure! # If it's a function, let it end with "Request" to identify them more easily if tlobject.is_function: result += 'Request' @@ -283,22 +308,25 @@ class TLGenerator: builder.writeln('if {}:'.format(name)) if arg.is_vector: - builder.writeln("writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID") + builder.writeln( + "writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID") builder.writeln('writer.write_int(len({}))'.format(name)) builder.writeln('for {}_item in {}:'.format(arg.name, name)) # Temporary disable .is_vector, not to enter this if again arg.is_vector = False - TLGenerator.write_onsend_code(builder, arg, args, name='{}_item'.format(arg.name)) + TLGenerator.write_onsend_code( + builder, arg, args, name='{}_item'.format(arg.name)) arg.is_vector = True elif arg.flag_indicator: # Calculate the flags with those items which are not None - builder.writeln('# Calculate the flags. This equals to those flag arguments which are NOT None') + builder.writeln( + '# Calculate the flags. This equals to those flag arguments which are NOT None') builder.writeln('flags = 0') for flag in args: if flag.is_flag: - builder.writeln('flags |= (1 << {}) if {} else 0' - .format(flag.flag_index, 'self.{}'.format(flag.name))) + builder.writeln('flags |= (1 << {}) if {} else 0'.format( + flag.flag_index, 'self.{}'.format(flag.name))) builder.writeln('writer.write_int(flags)') builder.writeln() @@ -310,10 +338,12 @@ class TLGenerator: builder.writeln('writer.write_long({})'.format(name)) elif 'int128' == arg.type: - builder.writeln('writer.write_large_int({}, bits=128)'.format(name)) + builder.writeln('writer.write_large_int({}, bits=128)'.format( + name)) elif 'int256' == arg.type: - builder.writeln('writer.write_large_int({}, bits=256)'.format(name)) + builder.writeln('writer.write_large_int({}, bits=256)'.format( + name)) elif 'double' == arg.type: builder.writeln('writer.write_double({})'.format(name)) @@ -366,7 +396,8 @@ class TLGenerator: was_flag = False if arg.is_flag: was_flag = True - builder.writeln('if (flags & (1 << {})) != 0:'.format(arg.flag_index)) + builder.writeln('if (flags & (1 << {})) != 0:'.format( + arg.flag_index)) # Temporary disable .is_flag not to enter this if again when calling the method recursively arg.is_flag = False @@ -377,7 +408,8 @@ class TLGenerator: builder.writeln('for _ in range({}_len):'.format(arg.name)) # Temporary disable .is_vector, not to enter this if again arg.is_vector = False - TLGenerator.write_onresponse_code(builder, arg, args, name='{}_item'.format(arg.name)) + TLGenerator.write_onresponse_code( + builder, arg, args, name='{}_item'.format(arg.name)) builder.writeln('{}.append({}_item)'.format(name, arg.name)) arg.is_vector = True @@ -393,10 +425,12 @@ class TLGenerator: builder.writeln('{} = reader.read_long()'.format(name)) elif 'int128' == arg.type: - builder.writeln('{} = reader.read_large_int(bits=128)'.format(name)) + builder.writeln('{} = reader.read_large_int(bits=128)'.format( + name)) elif 'int256' == arg.type: - builder.writeln('{} = reader.read_large_int(bits=256)'.format(name)) + builder.writeln('{} = reader.read_large_int(bits=256)'.format( + name)) elif 'double' == arg.type: builder.writeln('{} = reader.read_double()'.format(name)) @@ -408,7 +442,9 @@ class TLGenerator: builder.writeln('{} = reader.tgread_bool()'.format(name)) elif 'true' == arg.type: # Awkwardly enough, Telegram has both bool and "true", used in flags - builder.writeln('{} = True # Arbitrary not-None value, no need to read since it is a flag'.format(name)) + 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.tgread_bytes()'.format(name)) @@ -429,6 +465,7 @@ class TLGenerator: # Restore .is_flag arg.is_flag = True + if __name__ == '__main__': if TLGenerator.tlobjects_exist(): print('Detected previous TLObjects. Cleaning...') diff --git a/telethon_tests/__init__.py b/telethon_tests/__init__.py index 01f6aafe..64bb92a6 100644 --- a/telethon_tests/__init__.py +++ b/telethon_tests/__init__.py @@ -1,5 +1,5 @@ -from .crypto_tests import CryptoTests -from .network_tests import NetworkTests -from .parser_tests import ParserTests -from .tl_tests import TLTests -from .utils_tests import UtilsTests +from .crypto_test import CryptoTests +from .network_test import NetworkTests +from .parser_test import ParserTests +from .tl_test import TLTests +from .utils_test import UtilsTests diff --git a/try_telethon.py b/try_telethon.py index ae7f79d2..7b8cdee0 100644 --- a/try_telethon.py +++ b/try_telethon.py @@ -1,6 +1,7 @@ import traceback -from telethon.interactive_telegram_client import \ - InteractiveTelegramClient, print_title + +from telethon.interactive_telegram_client import (InteractiveTelegramClient, + print_title) def load_settings(path='api/settings'): @@ -34,7 +35,8 @@ if __name__ == '__main__': client.run() except Exception as e: - print('Unexpected error ({}): {} at\n{}'.format(type(e), e, traceback.format_exc())) + print('Unexpected error ({}): {} at\n{}'.format( + type(e), e, traceback.format_exc())) finally: print_title('Exit')