mirror of
				https://github.com/LonamiWebs/Telethon.git
				synced 2025-11-01 00:17:47 +03:00 
			
		
		
		
	Merge branch 'fdhadzh-master'
This commit is contained in:
		
						commit
						af9fbbb463
					
				
							
								
								
									
										22
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| -   repo: git://github.com/pre-commit/pre-commit-hooks | ||||
|     sha: 7539d8bd1a00a3c1bfd34cdb606d3a6372e83469 | ||||
|     hooks: | ||||
|     -   id: check-added-large-files | ||||
|     -   id: check-case-conflict | ||||
|     -   id: check-merge-conflict | ||||
|     -   id: check-symlinks | ||||
|     -   id: check-yaml | ||||
|     -   id: double-quote-string-fixer | ||||
|     -   id: end-of-file-fixer | ||||
|     -   id: name-tests-test | ||||
|     -   id: trailing-whitespace | ||||
| -   repo: git://github.com/pre-commit/mirrors-yapf | ||||
|     sha: v0.11.1 | ||||
|     hooks: | ||||
|     -   id: yapf | ||||
| -   repo: git://github.com/FalconSocial/pre-commit-python-sorter | ||||
|     sha: 1.0.4 | ||||
|     hooks: | ||||
|     -   id: python-import-sorter | ||||
|         args: | ||||
|         - --silent-overwrite | ||||
|  | @ -1,6 +1,5 @@ | |||
| import unittest | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     from telethon_tests import CryptoTests, ParserTests, TLTests, UtilsTests, NetworkTests | ||||
|     test_classes = [CryptoTests, ParserTests, TLTests, UtilsTests] | ||||
|  |  | |||
							
								
								
									
										22
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								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', | ||||
|         ], | ||||
|     } | ||||
| ) | ||||
|     }) | ||||
|  |  | |||
|  | @ -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] | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| from telethon.utils import BinaryWriter, BinaryReader | ||||
| import telethon.helpers as utils | ||||
| from telethon.utils import BinaryReader, BinaryWriter | ||||
| 
 | ||||
| 
 | ||||
| class AuthKey: | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 <path>: Uploads and sends a Photo located at the given path.') | ||||
|             print('  !uf <path>: Uploads and sends a File document located at the given path.') | ||||
|             print('  !dm <msg-id>: Downloads the given message Media (if any).') | ||||
|             print( | ||||
|                 '  !h: prints the latest messages (message History) of the chat.') | ||||
|             print( | ||||
|                 '  !up <path>: Uploads and sends a Photo located at the given path.') | ||||
|             print( | ||||
|                 '  !uf <path>: Uploads and sends a File document located at the given path.') | ||||
|             print( | ||||
|                 '  !dm <msg-id>: 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)) | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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: | ||||
|  | @ -62,7 +55,8 @@ class TelegramClient: | |||
|            .save() and .load() implementations to suit your needs.""" | ||||
| 
 | ||||
|         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 | ||||
|  | @ -73,9 +67,11 @@ class TelegramClient: | |||
|         elif isinstance(session, Session): | ||||
|             self.session = session | ||||
|         else: | ||||
|             raise ValueError('The given session must either be a string or a Session instance.') | ||||
|             raise ValueError( | ||||
|                 'The given session must either be a string or a Session instance.') | ||||
| 
 | ||||
|         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 | ||||
|  | @ -104,14 +100,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! | ||||
|  | @ -130,7 +129,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) | ||||
| 
 | ||||
|  | @ -191,11 +191,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_'): | ||||
|  | @ -205,10 +207,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 | ||||
|  | @ -221,11 +225,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() | ||||
|  | @ -245,29 +251,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) | ||||
|  | @ -275,15 +293,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 | ||||
| 
 | ||||
|  | @ -298,13 +324,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: | ||||
|  | @ -331,7 +359,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) | ||||
|  | @ -347,7 +376,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 | ||||
|  | @ -375,7 +408,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: | ||||
|  | @ -386,7 +419,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) | ||||
| 
 | ||||
|  | @ -397,17 +431,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 | ||||
|  | @ -431,28 +467,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 | ||||
| 
 | ||||
|  | @ -465,28 +509,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 | ||||
|  | @ -504,13 +560,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. | ||||
|  | @ -537,10 +600,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 | ||||
|  | @ -562,15 +629,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)""" | ||||
|  | @ -594,7 +667,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 | ||||
|  | @ -616,7 +690,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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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' | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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<REAL_TYPE> | ||||
|             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. | ||||
|  |  | |||
|  | @ -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...') | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -1,8 +1,7 @@ | |||
| import unittest | ||||
| 
 | ||||
| from telethon.crypto import AES | ||||
| import telethon.helpers as utils | ||||
| from telethon.crypto import Factorizator | ||||
| from telethon.crypto import AES, Factorizator | ||||
| 
 | ||||
| 
 | ||||
| class CryptoTests(unittest.TestCase): | ||||
|  | @ -38,14 +37,16 @@ class CryptoTests(unittest.TestCase): | |||
|              .format(value[:take], self.cipher_text[:take])) | ||||
| 
 | ||||
|         value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv) | ||||
|         assert value == self.cipher_text_padded, ('Ciphered text ("{}") does not equal expected ("{}")' | ||||
|                                                   .format(value, self.cipher_text_padded)) | ||||
|         assert value == self.cipher_text_padded, ( | ||||
|             'Ciphered text ("{}") does not equal expected ("{}")' | ||||
|             .format(value, self.cipher_text_padded)) | ||||
| 
 | ||||
|     def test_aes_decrypt(self): | ||||
|         # The ciphered text must always be padded | ||||
|         value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv) | ||||
|         assert value == self.plain_text_padded, ('Decrypted text ("{}") does not equal expected ("{}")' | ||||
|                                                  .format(value, self.plain_text_padded)) | ||||
|         assert value == self.plain_text_padded, ( | ||||
|             'Decrypted text ("{}") does not equal expected ("{}")' | ||||
|             .format(value, self.plain_text_padded)) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def test_calc_key(): | ||||
|  | @ -69,8 +70,10 @@ class CryptoTests(unittest.TestCase): | |||
|         expected_key = b"\xaf\xe3\x84Qm\xe0!\x0c\xd91\xe4\x9a\xa0v_gcx\xa1\xb0\xc9\xbc\x16'v\xcf,\x9dM\xae\xc6\xa5" | ||||
|         expected_iv = b'\xb8Q\xf3\xc5\xa3]\xc6\xdf\x9e\xe0Q\xbd"\x8d\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1\xa7\xa0\xf7\x0f' | ||||
| 
 | ||||
|         assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(expected_key, key) | ||||
|         assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(expected_iv, iv) | ||||
|         assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format( | ||||
|             expected_key, key) | ||||
|         assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format( | ||||
|             expected_iv, iv) | ||||
| 
 | ||||
|         # Calculate key being the server | ||||
|         msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]' | ||||
|  | @ -79,14 +82,17 @@ class CryptoTests(unittest.TestCase): | |||
|         expected_key = b'\xdd0X\xb6\x93\x8e\xc9y\xef\x83\xf8\x8cj\xa7h\x03\xe2\xc6\xb16\xc5\xbb\xfc\xe7\xdf\xd6\xb1g\xf7u\xcfk' | ||||
|         expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc' | ||||
| 
 | ||||
|         assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(expected_key, key) | ||||
|         assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(expected_iv, iv) | ||||
|         assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format( | ||||
|             expected_key, key) | ||||
|         assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format( | ||||
|             expected_iv, iv) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def test_calc_msg_key(): | ||||
|         value = utils.calc_msg_key(b'Some random message') | ||||
|         expected = b'\xdfAa\xfc\x10\xab\x89\xd2\xfe\x19C\xf1\xdd~\xbf\x81' | ||||
|         assert value == expected, 'Value ("{}") does not equal expected ("{}")'.format(value, expected) | ||||
|         assert value == expected, 'Value ("{}") does not equal expected ("{}")'.format( | ||||
|             value, expected) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def test_generate_key_data_from_nonces(): | ||||
|  | @ -97,8 +103,10 @@ class CryptoTests(unittest.TestCase): | |||
|         expected_key = b'?\xc4\xbd\xdf\rWU\x8a\xf5\x0f+V\xdc\x96up\x1d\xeeG\x00\x81|\x1eg\x8a\x8f{\xf0y\x80\xda\xde' | ||||
|         expected_iv = b'Q\x9dpZ\xb7\xdd\xcb\x82_\xfa\xf4\x90\xecn\x10\x9cD\xd2\x01\x8d\x83\xa0\xa4^\xb8\x91,\x7fI am' | ||||
| 
 | ||||
|         assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(key, expected_key) | ||||
|         assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format(key, expected_iv) | ||||
|         assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format( | ||||
|             key, expected_key) | ||||
|         assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format( | ||||
|             key, expected_iv) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def test_factorizator(): | ||||
|  | @ -3,8 +3,8 @@ import socket | |||
| import threading | ||||
| import unittest | ||||
| 
 | ||||
| from telethon.network import TcpTransport, TcpClient | ||||
| import telethon.network.authenticator as authenticator | ||||
| from telethon.network import TcpClient, TcpTransport | ||||
| 
 | ||||
| 
 | ||||
| def run_server_echo_thread(port): | ||||
|  | @ -31,7 +31,8 @@ class NetworkTests(unittest.TestCase): | |||
|         client = TcpClient() | ||||
|         client.connect('localhost', port) | ||||
|         client.write(msg) | ||||
|         assert msg == client.read(16), 'Read message does not equal sent message' | ||||
|         assert msg == client.read( | ||||
|             16), 'Read message does not equal sent message' | ||||
|         client.close() | ||||
| 
 | ||||
|     @staticmethod | ||||
|  | @ -14,7 +14,7 @@ class UtilsTests(unittest.TestCase): | |||
|             writer.write_float(17.0) | ||||
|             writer.write_double(25.0) | ||||
|             writer.write(bytes([26, 27, 28, 29, 30, 31, 32])) | ||||
|             writer.write_large_int(2 ** 127, 128, signed=False) | ||||
|             writer.write_large_int(2**127, 128, signed=False) | ||||
| 
 | ||||
|             data = writer.get_bytes() | ||||
|             expected = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88A\x00\x00\x00\x00\x00\x00' \ | ||||
|  | @ -24,26 +24,32 @@ class UtilsTests(unittest.TestCase): | |||
| 
 | ||||
|         with BinaryReader(data) as reader: | ||||
|             value = reader.read_byte() | ||||
|             assert value == 1, 'Example byte should be 1 but is {}'.format(value) | ||||
|             assert value == 1, 'Example byte should be 1 but is {}'.format( | ||||
|                 value) | ||||
| 
 | ||||
|             value = reader.read_int() | ||||
|             assert value == 5, 'Example integer should be 5 but is {}'.format(value) | ||||
|             assert value == 5, 'Example integer should be 5 but is {}'.format( | ||||
|                 value) | ||||
| 
 | ||||
|             value = reader.read_long() | ||||
|             assert value == 13, 'Example long integer should be 13 but is {}'.format(value) | ||||
|             assert value == 13, 'Example long integer should be 13 but is {}'.format( | ||||
|                 value) | ||||
| 
 | ||||
|             value = reader.read_float() | ||||
|             assert value == 17.0, 'Example float should be 17.0 but is {}'.format(value) | ||||
|             assert value == 17.0, 'Example float should be 17.0 but is {}'.format( | ||||
|                 value) | ||||
| 
 | ||||
|             value = reader.read_double() | ||||
|             assert value == 25.0, 'Example double should be 25.0 but is {}'.format(value) | ||||
|             assert value == 25.0, 'Example double should be 25.0 but is {}'.format( | ||||
|                 value) | ||||
| 
 | ||||
|             value = reader.read(7) | ||||
|             assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \ | ||||
|                 .format(bytes([26, 27, 28, 29, 30, 31, 32]), value) | ||||
| 
 | ||||
|             value = reader.read_large_int(128, signed=False) | ||||
|             assert value == 2 ** 127, 'Example large integer should be {} but is {}'.format(2 ** 127, value) | ||||
|             assert value == 2**127, 'Example large integer should be {} but is {}'.format( | ||||
|                 2**127, value) | ||||
| 
 | ||||
|         # Test Telegram that types are written right | ||||
|         with BinaryWriter() as writer: | ||||
|  | @ -51,12 +57,14 @@ class UtilsTests(unittest.TestCase): | |||
|             buffer = writer.get_bytes() | ||||
|             valid = b'\x78\x97\x46\x60'  # Tested written bytes using C#'s MemoryStream | ||||
| 
 | ||||
|             assert buffer == valid, "Written type should be {} but is {}".format(list(valid), list(buffer)) | ||||
|             assert buffer == valid, 'Written type should be {} but is {}'.format( | ||||
|                 list(valid), list(buffer)) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def test_binary_tgwriter_tgreader(): | ||||
|         small_data = os.urandom(33) | ||||
|         small_data_padded = os.urandom(19)  # +1 byte for length = 20 (evenly divisible by 4) | ||||
|         small_data_padded = os.urandom( | ||||
|             19)  # +1 byte for length = 20 (evenly divisible by 4) | ||||
| 
 | ||||
|         large_data = os.urandom(999) | ||||
|         large_data_padded = os.urandom(1024) | ||||
|  | @ -74,7 +82,9 @@ class UtilsTests(unittest.TestCase): | |||
|                 # And then try reading it without errors (it should be unharmed!) | ||||
|                 for datum in data: | ||||
|                     value = reader.tgread_bytes() | ||||
|                     assert value == datum, 'Example bytes should be {} but is {}'.format(datum, value) | ||||
|                     assert value == datum, 'Example bytes should be {} but is {}'.format( | ||||
|                         datum, value) | ||||
| 
 | ||||
|                 value = reader.tgread_string() | ||||
|                 assert value == string, 'Example string should be {} but is {}'.format(string, value) | ||||
|                 assert value == string, 'Example string should be {} but is {}'.format( | ||||
|                     string, value) | ||||
|  | @ -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') | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user