diff --git a/.gitignore b/.gitignore index aef0b91f..156d23e3 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ telethon/tl/functions/ telethon/tl/types/ telethon/tl/all_tlobjects.py +telethon/errors/rpc_error_list.py # User session *.session diff --git a/setup.py b/setup.py index 695ad1a5..13be0144 100755 --- a/setup.py +++ b/setup.py @@ -42,17 +42,26 @@ class TempWorkDir: os.chdir(self.original) +ERROR_LIST = 'telethon/errors/rpc_error_list.py' +ERRORS_JSON = 'telethon_generator/errors.json' +ERRORS_DESC = 'telethon_generator/error_descriptions' +SCHEME_TL = 'telethon_generator/scheme.tl' +GENERATOR_DIR = 'telethon/tl' +IMPORT_DEPTH = 2 + + def gen_tl(): from telethon_generator.tl_generator import TLGenerator - generator = TLGenerator('telethon/tl') + from telethon_generator.error_generator import generate_code + generator = TLGenerator(GENERATOR_DIR) if generator.tlobjects_exist(): print('Detected previous TLObjects. Cleaning...') generator.clean_tlobjects() print('Generating TLObjects...') - generator.generate_tlobjects( - 'telethon_generator/scheme.tl', import_depth=2 - ) + generator.generate_tlobjects(SCHEME_TL, import_depth=IMPORT_DEPTH) + print('Generating errors...') + generate_code(ERROR_LIST, json_file=ERRORS_JSON, errors_desc=ERRORS_DESC) print('Done.') @@ -63,7 +72,7 @@ def main(): elif len(argv) >= 2 and argv[1] == 'clean_tl': from telethon_generator.tl_generator import TLGenerator print('Cleaning...') - TLGenerator('telethon/tl').clean_tlobjects() + TLGenerator(GENERATOR_DIR).clean_tlobjects() print('Done.') elif len(argv) >= 2 and argv[1] == 'pypi': @@ -80,6 +89,10 @@ def main(): for x in ('build', 'dist', 'Telethon.egg-info'): rmtree(x, ignore_errors=True) + if len(argv) >= 2 and argv[1] == 'fetch_errors': + from telethon_generator.error_generator import fetch_errors + fetch_errors(ERRORS_JSON) + else: if not TelegramClient: gen_tl() diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index d65d426c..6e62bfb9 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -8,15 +8,8 @@ from .common import ( CdnFileTamperedError ) -from .rpc_errors import ( - RPCError, InvalidDCError, BadRequestError, UnauthorizedError, - ForbiddenError, NotFoundError, FloodError, ServerError, BadMessageError -) - -from .rpc_errors_303 import * -from .rpc_errors_400 import * -from .rpc_errors_401 import * -from .rpc_errors_420 import * +# This imports the base errors too, as they're imported there +from .rpc_error_list import * def report_error(code, message, report_method): @@ -43,27 +36,31 @@ def rpc_message_to_error(code, message, report_method=None): args=(code, message, report_method) ).start() - errors = { - 303: rpc_errors_303_all, - 400: rpc_errors_400_all, - 401: rpc_errors_401_all, - 420: rpc_errors_420_all - }.get(code, None) + # Try to get the error by direct look-up, otherwise regex + # TODO Maybe regexes could live in a separate dictionary? + cls = rpc_errors_all.get(message, None) + if cls: + return cls() - if errors is not None: - for msg, cls in errors.items(): - m = re.match(msg, message) - if m: - extra = int(m.group(1)) if m.groups() else None - return cls(extra=extra) + for msg_regex, cls in rpc_errors_all.items(): + m = re.match(msg_regex, message) + if m: + capture = int(m.group(1)) if m.groups() else None + return cls(capture=capture) - elif code == 403: + if code == 400: + return BadRequestError(message) + + if code == 401: + return UnauthorizedError(message) + + if code == 403: return ForbiddenError(message) - elif code == 404: + if code == 404: return NotFoundError(message) - elif code == 500: + if code == 500: return ServerError(message) return RPCError('{} (code {})'.format(message, code)) diff --git a/telethon/errors/rpc_errors.py b/telethon/errors/rpc_base_errors.py similarity index 100% rename from telethon/errors/rpc_errors.py rename to telethon/errors/rpc_base_errors.py diff --git a/telethon/errors/rpc_errors_303.py b/telethon/errors/rpc_errors_303.py deleted file mode 100644 index 21963154..00000000 --- a/telethon/errors/rpc_errors_303.py +++ /dev/null @@ -1,51 +0,0 @@ -from . import InvalidDCError - - -class FileMigrateError(InvalidDCError): - def __init__(self, **kwargs): - self.new_dc = kwargs['extra'] - super(Exception, self).__init__( - self, - 'The file to be accessed is currently stored in DC {}.' - .format(self.new_dc) - ) - - -class PhoneMigrateError(InvalidDCError): - def __init__(self, **kwargs): - self.new_dc = kwargs['extra'] - super(Exception, self).__init__( - self, - 'The phone number a user is trying to use for authorization is ' - 'associated with DC {}.' - .format(self.new_dc) - ) - - -class NetworkMigrateError(InvalidDCError): - def __init__(self, **kwargs): - self.new_dc = kwargs['extra'] - super(Exception, self).__init__( - self, - 'The source IP address is associated with DC {}.' - .format(self.new_dc) - ) - - -class UserMigrateError(InvalidDCError): - def __init__(self, **kwargs): - self.new_dc = kwargs['extra'] - super(Exception, self).__init__( - self, - 'The user whose identity is being used to execute queries is ' - 'associated with DC {}.' - .format(self.new_dc) - ) - - -rpc_errors_303_all = { - 'FILE_MIGRATE_(\d+)': FileMigrateError, - 'PHONE_MIGRATE_(\d+)': PhoneMigrateError, - 'NETWORK_MIGRATE_(\d+)': NetworkMigrateError, - 'USER_MIGRATE_(\d+)': UserMigrateError -} diff --git a/telethon/errors/rpc_errors_400.py b/telethon/errors/rpc_errors_400.py deleted file mode 100644 index 63f8dd0d..00000000 --- a/telethon/errors/rpc_errors_400.py +++ /dev/null @@ -1,453 +0,0 @@ -from . import BadRequestError - - -class ApiIdInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The api_id/api_hash combination is invalid.' - ) - - -class BotMethodInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The API access for bot users is restricted. The method you ' - 'tried to invoke cannot be executed as a bot.' - ) - - -class CdnMethodInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'This method cannot be invoked on a CDN server. Refer to ' - 'https://core.telegram.org/cdn#schema for available methods.' - ) - - -class ChannelInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'Invalid channel object. Make sure to pass the right types,' - ' for instance making sure that the request is designed for ' - 'channels or otherwise look for a different one more suited.' - ) - - -class ChannelPrivateError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The channel specified is private and you lack permission to ' - 'access it. Another reason may be that you were banned from it.' - ) - - -class ChatAdminRequiredError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - '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).' - ) - - -class ChatIdInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'Invalid object ID for a chat. Make sure to pass the right types,' - ' for instance making sure that the request is designed for chats' - ' (not channels/megagroups) or otherwise look for a different one' - ' more suited.\nAn example working with a megagroup and' - ' AddChatUserRequest, it will fail because megagroups are channels' - '. Use InviteToChannelRequest instead.' - ) - - -class ConnectionLangPackInvalid(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The specified language pack is not valid. This is meant to be ' - 'used by official applications only so far, leave it empty.' - ) - - -class ConnectionLayerInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The very first request must always be InvokeWithLayerRequest.' - ) - - -class DcIdInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'This occurs when an authorization is tried to be exported for ' - 'the same data center one is currently connected to.' - ) - - -class FieldNameEmptyError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The field with the name FIELD_NAME is missing.' - ) - - -class FieldNameInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The field with the name FIELD_NAME is invalid.' - ) - - -class FilePartsInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The number of file parts is invalid.' - ) - - -class FilePartMissingError(BadRequestError): - def __init__(self, **kwargs): - self.which = kwargs['extra'] - super(Exception, self).__init__( - self, - 'Part {} of the file is missing from storage.'.format(self.which) - ) - - -class FilePartInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The file part number is invalid.' - ) - - -class FirstNameInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The first name is invalid.' - ) - - -class InputMethodInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The invoked method does not exist anymore or has never existed.' - ) - - -class InputRequestTooLongError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The input request was too long. This may be a bug in the library ' - 'as it can occur when serializing more bytes than it should (like' - 'appending the vector constructor code at the end of a message).' - ) - - -class LastNameInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The last name is invalid.' - ) - - -class LimitInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'An invalid limit was provided. See ' - 'https://core.telegram.org/api/files#downloading-files' - ) - - -class LocationInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The location given for a file was invalid. See ' - 'https://core.telegram.org/api/files#downloading-files' - ) - - -class Md5ChecksumInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The MD5 check-sums do not match.' - ) - - -class MessageEmptyError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'Empty or invalid UTF-8 message was sent.' - ) - - -class MessageIdInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The specified message ID is invalid.' - ) - - -class MessageTooLongError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'Message was too long. Current maximum length is 4096 UTF-8 ' - 'characters.' - ) - - -class MessageNotModifiedError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'Content of the message was not modified.' - ) - - -class MsgWaitFailedError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'A waiting call returned an error.' - ) - - -class OffsetInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The given offset was invalid, it must be divisible by 1KB. ' - 'See https://core.telegram.org/api/files#downloading-files' - ) - - - -class PasswordHashInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The password (and thus its hash value) you entered is invalid.' - ) - - -class PeerIdInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'An invalid Peer was used. Make sure to pass the right peer type.' - ) - - -class PhoneCodeEmptyError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The phone code is missing.' - ) - - -class PhoneCodeExpiredError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The confirmation code has expired.' - ) - - -class PhoneCodeHashEmptyError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The phone code hash is missing.' - ) - - -class PhoneCodeInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The phone code entered was invalid.' - ) - - -class PhoneNumberBannedError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The used phone number has been banned from Telegram and cannot ' - 'be used anymore. Maybe check https://www.telegram.org/faq_spam.' - ) - - -class PhoneNumberInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The phone number is invalid.' - ) - - -class PhoneNumberOccupiedError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The phone number is already in use.' - ) - - -class PhoneNumberUnoccupiedError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The phone number is not yet being used.' - ) - - -class PhotoInvalidDimensionsError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The photo dimensions are invalid.' - ) - - -class TypeConstructorInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The type constructor is invalid.' - ) - - -class UsernameInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}".' - ) - - -class UsernameNotModifiedError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The username is not different from the current username.' - ) - - -class UsernameNotOccupiedError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The username is not in use by anyone else yet.' - ) - - -class UsernameOccupiedError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The username is already taken.' - ) - - -class UsersTooFewError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'Not enough users (to create a chat, for example).' - ) - - -class UsersTooMuchError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The maximum number of users has been exceeded (to create a ' - 'chat, for example).' - ) - - -class UserIdInvalidError(BadRequestError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'Invalid object ID for an user. Make sure to pass the right types,' - 'for instance making sure that the request is designed for users' - 'or otherwise look for a different one more suited.' - ) - - -rpc_errors_400_all = { - 'API_ID_INVALID': ApiIdInvalidError, - 'BOT_METHOD_INVALID': BotMethodInvalidError, - 'CDN_METHOD_INVALID': CdnMethodInvalidError, - 'CHANNEL_INVALID': ChannelInvalidError, - 'CHANNEL_PRIVATE': ChannelPrivateError, - 'CHAT_ADMIN_REQUIRED': ChatAdminRequiredError, - 'CHAT_ID_INVALID': ChatIdInvalidError, - 'CONNECTION_LAYER_INVALID': ConnectionLayerInvalidError, - 'DC_ID_INVALID': DcIdInvalidError, - 'FIELD_NAME_EMPTY': FieldNameEmptyError, - 'FIELD_NAME_INVALID': FieldNameInvalidError, - 'FILE_PARTS_INVALID': FilePartsInvalidError, - 'FILE_PART_(\d+)_MISSING': FilePartMissingError, - 'FILE_PART_INVALID': FilePartInvalidError, - 'FIRSTNAME_INVALID': FirstNameInvalidError, - 'INPUT_METHOD_INVALID': InputMethodInvalidError, - 'INPUT_REQUEST_TOO_LONG': InputRequestTooLongError, - 'LASTNAME_INVALID': LastNameInvalidError, - 'LIMIT_INVALID': LimitInvalidError, - 'LOCATION_INVALID': LocationInvalidError, - 'MD5_CHECKSUM_INVALID': Md5ChecksumInvalidError, - 'MESSAGE_EMPTY': MessageEmptyError, - 'MESSAGE_ID_INVALID': MessageIdInvalidError, - 'MESSAGE_TOO_LONG': MessageTooLongError, - 'MESSAGE_NOT_MODIFIED': MessageNotModifiedError, - 'MSG_WAIT_FAILED': MsgWaitFailedError, - 'OFFSET_INVALID': OffsetInvalidError, - 'PASSWORD_HASH_INVALID': PasswordHashInvalidError, - 'PEER_ID_INVALID': PeerIdInvalidError, - 'PHONE_CODE_EMPTY': PhoneCodeEmptyError, - 'PHONE_CODE_EXPIRED': PhoneCodeExpiredError, - 'PHONE_CODE_HASH_EMPTY': PhoneCodeHashEmptyError, - 'PHONE_CODE_INVALID': PhoneCodeInvalidError, - 'PHONE_NUMBER_BANNED': PhoneNumberBannedError, - 'PHONE_NUMBER_INVALID': PhoneNumberInvalidError, - 'PHONE_NUMBER_OCCUPIED': PhoneNumberOccupiedError, - 'PHONE_NUMBER_UNOCCUPIED': PhoneNumberUnoccupiedError, - 'PHOTO_INVALID_DIMENSIONS': PhotoInvalidDimensionsError, - 'TYPE_CONSTRUCTOR_INVALID': TypeConstructorInvalidError, - 'USERNAME_INVALID': UsernameInvalidError, - 'USERNAME_NOT_MODIFIED': UsernameNotModifiedError, - 'USERNAME_NOT_OCCUPIED': UsernameNotOccupiedError, - 'USERNAME_OCCUPIED': UsernameOccupiedError, - 'USERS_TOO_FEW': UsersTooFewError, - 'USERS_TOO_MUCH': UsersTooMuchError, - 'USER_ID_INVALID': UserIdInvalidError, -} diff --git a/telethon/errors/rpc_errors_401.py b/telethon/errors/rpc_errors_401.py deleted file mode 100644 index 5b22cb73..00000000 --- a/telethon/errors/rpc_errors_401.py +++ /dev/null @@ -1,98 +0,0 @@ -from . import UnauthorizedError - - -class ActiveUserRequiredError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The method is only available to already activated users.' - ) - - -class AuthKeyInvalidError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The key is invalid.' - ) - - -class AuthKeyPermEmptyError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The method is unavailable for temporary authorization key, not ' - 'bound to permanent.' - ) - - -class AuthKeyUnregisteredError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The key is not registered in the system.' - ) - - -class InviteHashExpiredError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The chat the user tried to join has expired and is not valid ' - 'anymore.' - ) - - -class SessionExpiredError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The authorization has expired.' - ) - - -class SessionPasswordNeededError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'Two-steps verification is enabled and a password is required.' - ) - - -class SessionRevokedError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The authorization has been invalidated, because of the user ' - 'terminating all sessions.' - ) - - -class UserAlreadyParticipantError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The authenticated user is already a participant of the chat.' - ) - - -class UserDeactivatedError(UnauthorizedError): - def __init__(self, **kwargs): - super(Exception, self).__init__( - self, - 'The user has been deleted/deactivated.' - ) - - -rpc_errors_401_all = { - 'ACTIVE_USER_REQUIRED': ActiveUserRequiredError, - 'AUTH_KEY_INVALID': AuthKeyInvalidError, - 'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError, - 'AUTH_KEY_UNREGISTERED': AuthKeyUnregisteredError, - 'INVITE_HASH_EXPIRED': InviteHashExpiredError, - 'SESSION_EXPIRED': SessionExpiredError, - 'SESSION_PASSWORD_NEEDED': SessionPasswordNeededError, - 'SESSION_REVOKED': SessionRevokedError, - 'USER_ALREADY_PARTICIPANT': UserAlreadyParticipantError, - 'USER_DEACTIVATED': UserDeactivatedError, -} diff --git a/telethon/errors/rpc_errors_420.py b/telethon/errors/rpc_errors_420.py deleted file mode 100644 index 8106cc5c..00000000 --- a/telethon/errors/rpc_errors_420.py +++ /dev/null @@ -1,16 +0,0 @@ -from . import FloodError - - -class FloodWaitError(FloodError): - def __init__(self, **kwargs): - self.seconds = kwargs['extra'] - super(Exception, self).__init__( - self, - 'A wait of {} seconds is required.' - .format(self.seconds) - ) - - -rpc_errors_420_all = { - 'FLOOD_WAIT_(\d+)': FloodWaitError -} diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 43232b0b..2355c6a4 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -129,13 +129,10 @@ class BinaryReader: return False # If there was still no luck, give up + self.seek(-4) # Go back raise TypeNotFoundError(constructor_id) - # Create an empty instance of the class and - # fill it with the read attributes - result = clazz.empty() - result.on_response(self) - return result + return clazz.from_reader(self) def tgread_vector(self): """Reads a vector (a list) of Telegram objects""" diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 6feb9841..5255513a 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -14,7 +14,7 @@ class TcpClient: if isinstance(timeout, timedelta): self.timeout = timeout.seconds - elif isinstance(timeout, int) or isinstance(timeout, float): + elif isinstance(timeout, (int, float)): self.timeout = float(timeout) else: raise ValueError('Invalid timeout type', type(timeout)) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index 1081897a..1accf493 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -42,7 +42,7 @@ def _do_authentication(connection): req_pq_request = ReqPqRequest( nonce=int.from_bytes(os.urandom(16), 'big', signed=True) ) - sender.send(req_pq_request.to_bytes()) + sender.send(bytes(req_pq_request)) with BinaryReader(sender.receive()) as reader: req_pq_request.on_response(reader) @@ -60,12 +60,12 @@ def _do_authentication(connection): p, q = rsa.get_byte_array(min(p, q)), rsa.get_byte_array(max(p, q)) new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True) - pq_inner_data = PQInnerData( + pq_inner_data = bytes(PQInnerData( pq=rsa.get_byte_array(pq), p=p, q=q, nonce=res_pq.nonce, server_nonce=res_pq.server_nonce, new_nonce=new_nonce - ).to_bytes() + )) # sha_digest + data + random_bytes cipher_text, target_fingerprint = None, None @@ -90,7 +90,7 @@ def _do_authentication(connection): public_key_fingerprint=target_fingerprint, encrypted_data=cipher_text ) - sender.send(req_dh_params.to_bytes()) + sender.send(bytes(req_dh_params)) # Step 2 response: DH Exchange with BinaryReader(sender.receive()) as reader: @@ -124,7 +124,6 @@ def _do_authentication(connection): raise AssertionError(server_dh_inner) if server_dh_inner.nonce != res_pq.nonce: - print(server_dh_inner.nonce, res_pq.nonce) raise SecurityError('Invalid nonce in encrypted answer') if server_dh_inner.server_nonce != res_pq.server_nonce: @@ -139,12 +138,12 @@ def _do_authentication(connection): gab = pow(g_a, b, dh_prime) # Prepare client DH Inner Data - client_dh_inner = ClientDHInnerData( + client_dh_inner = bytes(ClientDHInnerData( nonce=res_pq.nonce, server_nonce=res_pq.server_nonce, retry_id=0, # TODO Actual retry ID g_b=rsa.get_byte_array(gb) - ).to_bytes() + )) client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner @@ -157,7 +156,7 @@ def _do_authentication(connection): server_nonce=res_pq.server_nonce, encrypted_data=client_dh_encrypted, ) - sender.send(set_client_dh.to_bytes()) + sender.send(bytes(set_client_dh)) # Step 3 response: Complete DH Exchange with BinaryReader(sender.receive()) as reader: diff --git a/telethon/network/connection.py b/telethon/network/connection.py index 2500c0c1..fe04352f 100644 --- a/telethon/network/connection.py +++ b/telethon/network/connection.py @@ -141,28 +141,25 @@ class Connection: raise ValueError('Invalid connection mode specified: ' + str(self._mode)) def _recv_tcp_full(self): - packet_length_bytes = self.read(4) - packet_length = int.from_bytes(packet_length_bytes, 'little') + packet_len_seq = self.read(8) # 4 and 4 + packet_len, seq = struct.unpack('= 127: - length = int.from_bytes(self.read(3) + b'\0', 'little') + length = struct.unpack(' self.session.flood_sleep_threshold | 0: + raise + + self._logger.debug( + 'Sleep of %d seconds below threshold, sleeping' % e.seconds + ) + sleep(e.seconds) # Some really basic functionality @@ -683,10 +699,8 @@ class TelegramBareClient: cdn_decrypter = None try: - offset_index = 0 + offset = 0 while True: - offset = offset_index * part_size - try: if cdn_decrypter: result = cdn_decrypter.get_file() @@ -705,7 +719,7 @@ class TelegramBareClient: client = self._get_exported_client(e.new_dc) continue - offset_index += 1 + offset += part_size # If we have received no data (0 bytes), the file is over # So there is nothing left to download and write @@ -742,10 +756,10 @@ class TelegramBareClient: def add_update_handler(self, handler): """Adds an update handler (a function which takes a TLObject, an update, as its parameter) and listens for updates""" - sync = not self.updates.handlers + if not self.updates.get_workers: + warnings.warn("There are no update workers running, so adding an update handler will have no effect.") + self.updates.handlers.append(handler) - if sync: - self.sync_updates() def remove_update_handler(self, handler): self.updates.handlers.remove(handler) @@ -801,7 +815,9 @@ class TelegramBareClient: try: import socks - if isinstance(error, socks.GeneralProxyError): + if isinstance(error, ( + socks.GeneralProxyError, socks.ProxyConnectionError + )): # This is a known error, and it's not related to # Telegram but rather to the proxy. Disconnect and # hand it over to the main thread. diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index bdaa41d8..3a1ba20e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -15,6 +15,7 @@ from .errors import ( ) from .network import ConnectionMode from .tl import TLObject +from .tl.custom import Draft from .tl.entity_database import EntityDatabase from .tl.functions.account import ( GetPasswordRequest @@ -28,8 +29,8 @@ from .tl.functions.contacts import ( ) from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest, - SendMessageRequest, GetChatsRequest -) + SendMessageRequest, GetChatsRequest, + GetAllDraftsRequest) from .tl.functions import channels from .tl.functions import messages @@ -46,7 +47,7 @@ from .tl.types import ( InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty, Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, - UpdateNewMessage, UpdateShortSentMessage, + UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel) from .tl.types.messages import DialogsSlice @@ -252,7 +253,7 @@ class TelegramClient(TelegramBareClient): if limit is None: limit = float('inf') - dialogs = {} # Use Dialog.top_message as identifier to avoid dupes + dialogs = {} # Use peer id as identifier to avoid dupes messages = {} # Used later for sorting TODO also return these? entities = {} while len(dialogs) < limit: @@ -267,7 +268,7 @@ class TelegramClient(TelegramBareClient): break for d in r.dialogs: - dialogs[d.top_message] = d + dialogs[utils.get_peer_id(d.peer, True)] = d for m in r.messages: messages[m.id] = m @@ -302,9 +303,20 @@ class TelegramClient(TelegramBareClient): [utils.find_user_or_chat(d.peer, entities, entities) for d in ds] ) - # endregion + def get_drafts(self): # TODO: Ability to provide a `filter` + """ + Gets all open draft messages. - # region Message requests + Returns a list of custom `Draft` objects that are easy to work with: You can call + `draft.set_message('text')` to change the message, or delete it through `draft.delete()`. + + :return List[telethon.tl.custom.Draft]: A list of open drafts + """ + response = self(GetAllDraftsRequest()) + self.session.process_entities(response) + self.session.generate_sequence(response.seq) + drafts = [Draft._from_update(self, u) for u in response.updates] + return drafts def send_message(self, entity, @@ -347,7 +359,7 @@ class TelegramClient(TelegramBareClient): break for update in result.updates: - if isinstance(update, UpdateNewMessage): + if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)): if update.message.id == msg_id: return update.message @@ -488,9 +500,13 @@ class TelegramClient(TelegramBareClient): def send_file(self, entity, file, caption='', force_document=False, progress_callback=None, reply_to=None, + attributes=None, **kwargs): """Sends a file to the specified entity. The file may either be a path, a byte array, or a stream. + Note that if a byte array or a stream is given, a filename + or its type won't be inferred, and it will be sent as an + "unnamed application/octet-stream". An optional caption can also be specified for said file. @@ -507,6 +523,10 @@ class TelegramClient(TelegramBareClient): The "reply_to" parameter works exactly as the one on .send_message. + If "attributes" is set to be a list of DocumentAttribute's, these + will override the automatically inferred ones (so that you can + modify the file name of the file sent for instance). + If "is_voice_note" in kwargs, despite its value, and the file is sent as a document, it will be sent as a voice note. @@ -537,16 +557,28 @@ class TelegramClient(TelegramBareClient): # Determine mime-type and attributes # Take the first element by using [0] since it returns a tuple mime_type = guess_type(file)[0] - attributes = [ + attr_dict = { + DocumentAttributeFilename: DocumentAttributeFilename(os.path.basename(file)) # TODO If the input file is an audio, find out: # Performer and song title and add DocumentAttributeAudio - ] + } else: - attributes = [DocumentAttributeFilename('unnamed')] + attr_dict = { + DocumentAttributeFilename: + DocumentAttributeFilename('unnamed') + } if 'is_voice_note' in kwargs: - attributes.append(DocumentAttributeAudio(0, voice=True)) + attr_dict[DocumentAttributeAudio] = \ + DocumentAttributeAudio(0, voice=True) + + # Now override the attributes if any. As we have a dict of + # {cls: instance}, we can override any class with the list + # of attributes provided by the user easily. + if attributes: + for a in attributes: + attr_dict[type(a)] = a # Ensure we have a mime type, any; but it cannot be None # 'The "octet-stream" subtype is used to indicate that a body @@ -557,7 +589,7 @@ class TelegramClient(TelegramBareClient): media = InputMediaUploadedDocument( file=file_handle, mime_type=mime_type, - attributes=attributes, + attributes=list(attr_dict.values()), caption=caption ) @@ -873,24 +905,23 @@ class TelegramClient(TelegramBareClient): pass if isinstance(entity, int) or ( - isinstance(entity, TLObject) and + isinstance(entity, TLObject) and # crc32(b'InputPeer') and crc32(b'Peer') - type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)): + type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)): ie = self.get_input_entity(entity) - result = None if isinstance(ie, InputPeerUser): - result = self(GetUsersRequest([ie])) + self(GetUsersRequest([ie])) elif isinstance(ie, InputPeerChat): - result = self(GetChatsRequest([ie.chat_id])) + self(GetChatsRequest([ie.chat_id])) elif isinstance(ie, InputPeerChannel): - result = self(GetChannelsRequest([ie])) - - if result: - self.session.process_entities(result) - try: - return self.session.entities[ie] - except KeyError: - pass + self(GetChannelsRequest([ie])) + try: + # session.process_entities has been called in the MtProtoSender + # with the result of these calls, so they should now be on the + # entities database. + return self.session.entities[ie] + except KeyError: + pass if isinstance(entity, str): return self._get_entity_from_string(entity) @@ -906,11 +937,11 @@ class TelegramClient(TelegramBareClient): phone = EntityDatabase.parse_phone(string) if phone: entity = phone - self.session.process_entities(self(GetContactsRequest(0))) + self(GetContactsRequest(0)) else: entity = string.strip('@').lower() - self.session.process_entities(self(ResolveUsernameRequest(entity))) - + self(ResolveUsernameRequest(entity)) + # MtProtoSender will call .process_entities on the requests made try: return self.session.entities[entity] except KeyError: @@ -956,9 +987,17 @@ class TelegramClient(TelegramBareClient): ) if self.session.save_entities: - # Not found, look in the dialogs (this will save the users) - self.get_dialogs(limit=None) - + # Not found, look in the latest dialogs. + # This is useful if for instance someone just sent a message but + # the updates didn't specify who, as this person or chat should + # be in the latest dialogs. + self(GetDialogsRequest( + offset_date=None, + offset_id=0, + offset_peer=InputPeerEmpty(), + limit=0, + exclude_pinned=True + )) try: return self.session.entities.get_input_entity(peer) except KeyError: diff --git a/telethon/tl/custom/__init__.py b/telethon/tl/custom/__init__.py new file mode 100644 index 00000000..40914f16 --- /dev/null +++ b/telethon/tl/custom/__init__.py @@ -0,0 +1 @@ +from .draft import Draft diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py new file mode 100644 index 00000000..c50baa78 --- /dev/null +++ b/telethon/tl/custom/draft.py @@ -0,0 +1,80 @@ +from ..functions.messages import SaveDraftRequest +from ..types import UpdateDraftMessage + + +class Draft: + """ + Custom class that encapsulates a draft on the Telegram servers, providing + an abstraction to change the message conveniently. The library will return + instances of this class when calling `client.get_drafts()`. + """ + def __init__(self, client, peer, draft): + self._client = client + self._peer = peer + + self.text = draft.message + self.date = draft.date + self.no_webpage = draft.no_webpage + self.reply_to_msg_id = draft.reply_to_msg_id + self.entities = draft.entities + + @classmethod + def _from_update(cls, client, update): + if not isinstance(update, UpdateDraftMessage): + raise ValueError( + 'You can only create a new `Draft` from a corresponding ' + '`UpdateDraftMessage` object.' + ) + + return cls(client=client, peer=update.peer, draft=update.draft) + + @property + def entity(self): + return self._client.get_entity(self._peer) + + @property + def input_entity(self): + return self._client.get_input_entity(self._peer) + + def set_message(self, text, no_webpage=None, reply_to_msg_id=None, entities=None): + """ + Changes the draft message on the Telegram servers. The changes are + reflected in this object. Changing only individual attributes like for + example the `reply_to_msg_id` should be done by providing the current + values of this object, like so: + + draft.set_message( + draft.text, + no_webpage=draft.no_webpage, + reply_to_msg_id=NEW_VALUE, + entities=draft.entities + ) + + :param str text: New text of the draft + :param bool no_webpage: Whether to attach a web page preview + :param int reply_to_msg_id: Message id to reply to + :param list entities: A list of formatting entities + :return bool: `True` on success + """ + result = self._client(SaveDraftRequest( + peer=self._peer, + message=text, + no_webpage=no_webpage, + reply_to_msg_id=reply_to_msg_id, + entities=entities + )) + + if result: + self.text = text + self.no_webpage = no_webpage + self.reply_to_msg_id = reply_to_msg_id + self.entities = entities + + return result + + def delete(self): + """ + Deletes this draft + :return bool: `True` on success + """ + return self.set_message(text='') diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index 61c07efc..554e2a5a 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -74,9 +74,7 @@ class EntityDatabase: getattr(p, 'access_hash', 0) # chats won't have hash if self.enabled_full: - if isinstance(e, User) \ - or isinstance(e, Chat) \ - or isinstance(e, Channel): + if isinstance(e, (User, Chat, Channel)): new.append(e) except ValueError: pass @@ -123,47 +121,64 @@ class EntityDatabase: if phone: self._username_id[phone] = marked_id - def __getitem__(self, key): - """Accepts a digit only string as phone number, - otherwise it's treated as an username. + def _parse_key(self, key): + """Parses the given string, integer or TLObject key into a + marked user ID ready for use on self._entities. - If an integer is given, it's treated as the ID of the desired User. - The ID given won't try to be guessed as the ID of a chat or channel, - as there may be an user with that ID, and it would be unreliable. + If a callable key is given, the entity will be passed to the + function, and if it returns a true-like value, the marked ID + for such entity will be returned. - If a Peer is given (PeerUser, PeerChat, PeerChannel), - its specific entity is retrieved as User, Chat or Channel. - Note that megagroups are channels with .megagroup = True. + Raises ValueError if it cannot be parsed. """ if isinstance(key, str): phone = EntityDatabase.parse_phone(key) - if phone: - return self._phone_id[phone] - else: - key = key.lstrip('@').lower() - return self._entities[self._username_id[key]] + try: + if phone: + return self._phone_id[phone] + else: + return self._username_id[key.lstrip('@').lower()] + except KeyError as e: + raise ValueError() from e if isinstance(key, int): - return self._entities[key] # normal IDs are assumed users + return key # normal IDs are assumed users if isinstance(key, TLObject): - sc = type(key).SUBCLASS_OF_ID - if sc == 0x2d45687: - # Subclass of "Peer" - return self._entities[utils.get_peer_id(key, add_mark=True)] - elif sc in {0x2da17977, 0xc5af5d94, 0x6d44b7db}: - # Subclass of "User", "Chat" or "Channel" - return key + return utils.get_peer_id(key, add_mark=True) - raise KeyError(key) + if callable(key): + for k, v in self._entities.items(): + if key(v): + return k + + raise ValueError() + + def __getitem__(self, key): + """See the ._parse_key() docstring for possible values of the key""" + try: + return self._entities[self._parse_key(key)] + except (ValueError, KeyError) as e: + raise KeyError(key) from e def __delitem__(self, key): - target = self[key] - del self._entities[key] - if getattr(target, 'username'): - del self._username_id[target.username] + try: + old = self._entities.pop(self._parse_key(key)) + # Try removing the username and phone (if pop didn't fail), + # since the entity may have no username or phone, just ignore + # errors. It should be there if we popped the entity correctly. + try: + del self._username_id[getattr(old, 'username', None)] + except KeyError: + pass - # TODO Allow search by name by tokenizing the input and return a list + try: + del self._phone_id[getattr(old, 'phone', None)] + except KeyError: + pass + + except (ValueError, KeyError) as e: + raise KeyError(key) from e @staticmethod def parse_phone(phone): @@ -177,8 +192,10 @@ class EntityDatabase: def get_input_entity(self, peer): try: - i, k = utils.get_peer_id(peer, add_mark=True, get_kind=True) - h = self._input_entities[i] + i = utils.get_peer_id(peer, add_mark=True) + h = self._input_entities[i] # we store the IDs marked + i, k = utils.resolve_id(i) # removes the mark and returns kind + if k == PeerUser: return InputPeerUser(i, h) elif k == PeerChat: diff --git a/telethon/tl/gzip_packed.py b/telethon/tl/gzip_packed.py index 05453d4b..053acd86 100644 --- a/telethon/tl/gzip_packed.py +++ b/telethon/tl/gzip_packed.py @@ -13,26 +13,26 @@ class GzipPacked(TLObject): @staticmethod def gzip_if_smaller(request): - """Calls request.to_bytes(), and based on a certain threshold, + """Calls bytes(request), and based on a certain threshold, optionally gzips the resulting data. If the gzipped data is smaller than the original byte array, this is returned instead. Note that this only applies to content related requests. """ - data = request.to_bytes() + data = bytes(request) # TODO This threshold could be configurable if request.content_related and len(data) > 512: - gzipped = GzipPacked(data).to_bytes() + gzipped = bytes(GzipPacked(data)) return gzipped if len(gzipped) < len(data) else data else: return data - def to_bytes(self): + def __bytes__(self): # TODO Maybe compress level could be an option return struct.pack(' log works - i = -(i + pow(10, math.floor(math.log10(i) + 3))) + return -(i + pow(10, math.floor(math.log10(i) + 3))) + else: + return i - return (i, k) if get_kind else i # return kind only if get_kind + _raise_cast_fail(peer, 'int') def resolve_id(marked_id): @@ -384,11 +376,7 @@ def find_message(update): def get_appropriated_part_size(file_size): """Gets the appropriated part size when uploading or downloading files, given an initial file size""" - if file_size <= 1048576: # 1MB - return 32 - if file_size <= 10485760: # 10MB - return 64 - if file_size <= 393216000: # 375MB + if file_size <= 104857600: # 100MB return 128 if file_size <= 786432000: # 750MB return 256 diff --git a/telethon_generator/error_descriptions b/telethon_generator/error_descriptions new file mode 100644 index 00000000..f0a14e68 --- /dev/null +++ b/telethon_generator/error_descriptions @@ -0,0 +1,65 @@ +# These are comments. Spaces around the = are optional. Empty lines ignored. +#CODE=Human readable description + +FILE_MIGRATE_X=The file to be accessed is currently stored in DC {} +PHONE_MIGRATE_X=The phone number a user is trying to use for authorization is associated with DC {} +NETWORK_MIGRATE_X=The source IP address is associated with DC {} +USER_MIGRATE_X=The user whose identity is being used to execute queries is associated with DC {} +API_ID_INVALID=The api_id/api_hash combination is invalid +BOT_METHOD_INVALID=The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot +CDN_METHOD_INVALID=This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods +CHANNEL_INVALID=Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited +CHANNEL_PRIVATE=The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it +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) +CHAT_ID_INVALID=Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead +CONNECTION_LANG_PACK_INVALID=The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty +CONNECTION_LAYER_INVALID=The very first request must always be InvokeWithLayerRequest +DC_ID_INVALID=This occurs when an authorization is tried to be exported for the same data center one is currently connected to +FIELD_NAME_EMPTY=The field with the name FIELD_NAME is missing +FIELD_NAME_INVALID=The field with the name FIELD_NAME is invalid +FILE_PARTS_INVALID=The number of file parts is invalid +FILE_PART_X_MISSING=Part {} of the file is missing from storage +FILE_PART_INVALID=The file part number is invalid +FIRSTNAME_INVALID=The first name is invalid +INPUT_METHOD_INVALID=The invoked method does not exist anymore or has never existed +INPUT_REQUEST_TOO_LONG=The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (likeappending the vector constructor code at the end of a message) +LASTNAME_INVALID=The last name is invalid +LIMIT_INVALID=An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files +LOCATION_INVALID=The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files +MD5_CHECKSUM_INVALID=The MD5 check-sums do not match +MESSAGE_EMPTY=Empty or invalid UTF-8 message was sent +MESSAGE_ID_INVALID=The specified message ID is invalid +MESSAGE_TOO_LONG=Message was too long. Current maximum length is 4096 UTF-8 characters +MESSAGE_NOT_MODIFIED=Content of the message was not modified +MSG_WAIT_FAILED=A waiting call returned an error +OFFSET_INVALID=The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files +PASSWORD_HASH_INVALID=The password (and thus its hash value) you entered is invalid +PEER_ID_INVALID=An invalid Peer was used. Make sure to pass the right peer type +PHONE_CODE_EMPTY=The phone code is missing +PHONE_CODE_EXPIRED=The confirmation code has expired +PHONE_CODE_HASH_EMPTY=The phone code hash is missing +PHONE_CODE_INVALID=The phone code entered was invalid +PHONE_NUMBER_BANNED=The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam +PHONE_NUMBER_INVALID=The phone number is invalid +PHONE_NUMBER_OCCUPIED=The phone number is already in use +PHONE_NUMBER_UNOCCUPIED=The phone number is not yet being used +PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid +TYPE_CONSTRUCTOR_INVALID=The type constructor is invalid +USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}" +USERNAME_NOT_MODIFIED=The username is not different from the current username +USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet +USERNAME_OCCUPIED=The username is already taken +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) +USER_ID_INVALID=Invalid object ID for an user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited +ACTIVE_USER_REQUIRED=The method is only available to already activated users +AUTH_KEY_INVALID=The key is invalid +AUTH_KEY_PERM_EMPTY=The method is unavailable for temporary authorization key, not bound to permanent +AUTH_KEY_UNREGISTERED=The key is not registered in the system +INVITE_HASH_EXPIRED=The chat the user tried to join has expired and is not valid anymore +SESSION_EXPIRED=The authorization has expired +SESSION_PASSWORD_NEEDED=Two-steps verification is enabled and a password is required +SESSION_REVOKED=The authorization has been invalidated, because of the user terminating all sessions +USER_ALREADY_PARTICIPANT=The authenticated user is already a participant of the chat +USER_DEACTIVATED=The user has been deleted/deactivated +FLOOD_WAIT_X=A wait of {} seconds is required diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py new file mode 100644 index 00000000..81bcda5c --- /dev/null +++ b/telethon_generator/error_generator.py @@ -0,0 +1,170 @@ +import json +import re +import urllib.request +from collections import defaultdict + +URL = 'https://rpc.pwrtelegram.xyz/?all' + +known_base_classes = { + 303: 'InvalidDCError', + 400: 'BadRequestError', + 401: 'UnauthorizedError', + 403: 'ForbiddenError', + 404: 'NotFoundError', + 420: 'FloodError', + 500: 'ServerError', +} + +# The API doesn't return the code for some (vital) errors. They are +# all assumed to be 400, except these well-known ones that aren't. +known_codes = { + 'ACTIVE_USER_REQUIRED': 401, + 'AUTH_KEY_UNREGISTERED': 401, + 'USER_DEACTIVATED': 401 +} + + +def fetch_errors(output, url=URL): + print('Opening a connection to', url, '...') + r = urllib.request.urlopen(url) + print('Checking response...') + data = json.loads( + r.read().decode(r.info().get_param('charset') or 'utf-8') + ) + if data.get('ok'): + print('Response was okay, saving data') + with open(output, 'w', encoding='utf-8') as f: + json.dump(data, f) + return True + else: + print('The data received was not okay:') + print(json.dumps(data, indent=4)) + return False + + +def get_class_name(error_code): + if isinstance(error_code, int): + return known_base_classes.get( + error_code, 'RPCError' + str(error_code).replace('-', 'Neg') + ) + + if 'FIRSTNAME' in error_code: + error_code = error_code.replace('FIRSTNAME', 'FIRST_NAME') + + result = re.sub( + r'_([a-z])', lambda m: m.group(1).upper(), error_code.lower() + ) + return result[:1].upper() + result[1:].replace('_', '') + 'Error' + + +def write_error(f, code, name, desc, capture_name): + f.write( + f'\n' + f'\n' + f'class {name}({get_class_name(code)}):\n' + f' def __init__(self, **kwargs):\n' + f' ' + ) + if capture_name: + f.write( + f"self.{capture_name} = int(kwargs.get('capture', 0))\n" + f" " + ) + f.write(f'super(Exception, self).__init__(self, {repr(desc)}') + if capture_name: + f.write(f'.format(self.{capture_name})') + f.write(')\n') + + +def generate_code(output, json_file, errors_desc): + with open(json_file, encoding='utf-8') as f: + data = json.load(f) + + errors = defaultdict(set) + # PWRTelegram's API doesn't return all errors, which we do need here. + # Add some special known-cases manually first. + errors[420].add('FLOOD_WAIT_X') + errors[401].update(( + 'AUTH_KEY_INVALID', 'SESSION_EXPIRED', 'SESSION_REVOKED' + )) + errors[303].update(( + 'FILE_MIGRATE_X', 'PHONE_MIGRATE_X', + 'NETWORK_MIGRATE_X', 'USER_MIGRATE_X' + )) + for error_code, method_errors in data['result'].items(): + for error_list in method_errors.values(): + for error in error_list: + errors[int(error_code)].add(re.sub('_\d+', '_X', error).upper()) + + # Some errors are in the human result, but not with a code. Assume code 400 + for error in data['human_result']: + if error[0] != '-' and not error.isdigit(): + error = re.sub('_\d+', '_X', error).upper() + if not any(error in es for es in errors.values()): + errors[known_codes.get(error, 400)].add(error) + + # Some error codes are not known, so create custom base classes if needed + needed_base_classes = [ + (e, get_class_name(e)) for e in errors if e not in known_base_classes + ] + + # Prefer the descriptions that are related with Telethon way of coding to + # those that PWRTelegram's API provides. + telethon_descriptions = {} + with open(errors_desc, encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + equal = line.index('=') + message, description = line[:equal], line[equal + 1:] + telethon_descriptions[message.rstrip()] = description.lstrip() + + # Names for the captures, or 'x' if unknown + capture_names = { + 'FloodWaitError': 'seconds', + 'FileMigrateError': 'new_dc', + 'NetworkMigrateError': 'new_dc', + 'PhoneMigrateError': 'new_dc', + 'UserMigrateError': 'new_dc', + 'FilePartMissingError': 'which' + } + + # Everything ready, generate the code + with open(output, 'w', encoding='utf-8') as f: + f.write( + f'from .rpc_base_errors import RPCError, BadMessageError, ' + f'{", ".join(known_base_classes.values())}\n' + ) + for code, cls in needed_base_classes: + f.write( + f'\n' + f'\n' + f'class {cls}(RPCError):\n' + f' code = {code}\n' + ) + + patterns = [] # Save this dictionary later in the generated code + for error_code, error_set in errors.items(): + for error in sorted(error_set): + description = telethon_descriptions.get( + error, '\n'.join(data['human_result'].get( + error, ['No description known.'] + )) + ) + has_captures = '_X' in error + if has_captures: + name = get_class_name(error.replace('_X', '')) + pattern = error.replace('_X', r'_(\d+)') + else: + name, pattern = get_class_name(error), error + + patterns.append((pattern, name)) + capture = capture_names.get(name, 'x') if has_captures else None + # TODO Some errors have the same name but different code, + # split this accross different files? + write_error(f, error_code, name, description, capture) + + f.write('\n\nrpc_errors_all = {\n') + for pattern, name in patterns: + f.write(f' {repr(pattern)}: {name},\n') + f.write('}\n') diff --git a/telethon_generator/errors.json b/telethon_generator/errors.json new file mode 100644 index 00000000..e807ff2d --- /dev/null +++ b/telethon_generator/errors.json @@ -0,0 +1 @@ +{"ok": true, "result": {"400": {"account.updateProfile": ["ABOUT_TOO_LONG", "FIRSTNAME_INVALID"], "auth.importBotAuthorization": ["ACCESS_TOKEN_EXPIRED", "ACCESS_TOKEN_INVALID", "API_ID_INVALID"], "auth.sendCode": ["API_ID_INVALID", "INPUT_REQUEST_TOO_LONG", "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN", "PHONE_NUMBER_BANNED", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_PASSWORD_PROTECTED"], "messages.setInlineBotResults": ["ARTICLE_TITLE_EMPTY", "BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "MESSAGE_EMPTY", "QUERY_ID_INVALID", "REPLY_MARKUP_INVALID", "RESULT_TYPE_INVALID", "SEND_MESSAGE_TYPE_INVALID", "START_PARAM_INVALID"], "auth.importAuthorization": ["AUTH_BYTES_INVALID", "USER_ID_INVALID"], "invokeWithLayer": ["AUTH_BYTES_INVALID", "CDN_METHOD_INVALID", "CONNECTION_API_ID_INVALID", "CONNECTION_LANG_PACK_INVALID", "INPUT_LAYER_INVALID"], "channels.inviteToChannel": ["BOT_GROUPS_BLOCKED", "BOTS_TOO_MUCH", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_USER_DEACTIVATED", "USER_BANNED_IN_CHANNEL", "USER_BLOCKED", "USER_BOT", "USER_ID_INVALID", "USER_KICKED", "USER_NOT_MUTUAL_CONTACT", "USERS_TOO_MUCH"], "messages.getInlineBotResults": ["BOT_INLINE_DISABLED", "BOT_INVALID"], "messages.startBot": ["BOT_INVALID", "PEER_ID_INVALID", "START_PARAM_EMPTY", "START_PARAM_INVALID"], "messages.uploadMedia": ["BOT_MISSING", "PEER_ID_INVALID"], "stickers.addStickerToSet": ["BOT_MISSING", "STICKERSET_INVALID"], "stickers.changeStickerPosition": ["BOT_MISSING", "STICKER_INVALID"], "stickers.createStickerSet": ["BOT_MISSING", "PACK_SHORT_NAME_INVALID", "PEER_ID_INVALID", "STICKERS_EMPTY", "USER_ID_INVALID"], "stickers.removeStickerFromSet": ["BOT_MISSING", "STICKER_INVALID"], "messages.sendMessage": ["BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "ENTITY_MENTION_USER_INVALID", "INPUT_USER_DEACTIVATED", "MESSAGE_EMPTY", "MESSAGE_TOO_LONG", "PEER_ID_INVALID", "REPLY_MARKUP_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "phone.acceptCall": ["CALL_ALREADY_ACCEPTED", "CALL_ALREADY_DECLINED", "CALL_PEER_INVALID", "CALL_PROTOCOL_FLAGS_INVALID"], "phone.discardCall": ["CALL_ALREADY_ACCEPTED", "CALL_PEER_INVALID"], "phone.confirmCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.receivedCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.saveCallDebug": ["CALL_PEER_INVALID", "DATA_JSON_INVALID"], "phone.setCallRating": ["CALL_PEER_INVALID"], "phone.requestCall": ["CALL_PROTOCOL_FLAGS_INVALID", "PARTICIPANT_VERSION_OUTDATED", "USER_ID_INVALID"], "updates.getDifference": ["CDN_METHOD_INVALID", "DATE_EMPTY", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID"], "upload.getCdnFileHashes": ["CDN_METHOD_INVALID", "RSA_DECRYPT_FAILED"], "channels.checkUsername": ["CHANNEL_INVALID", "CHAT_ID_INVALID", "USERNAME_INVALID"], "channels.deleteChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteUserHistory": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.editAbout": ["CHANNEL_INVALID", "CHAT_ABOUT_NOT_MODIFIED", "CHAT_ABOUT_TOO_LONG", "CHAT_ADMIN_REQUIRED"], "channels.editAdmin": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_CREATOR", "USER_ID_INVALID", "USER_NOT_MUTUAL_CONTACT"], "channels.editBanned": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "USER_ADMIN_INVALID", "USER_ID_INVALID"], "channels.editPhoto": ["CHANNEL_INVALID"], "channels.editTitle": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.exportInvite": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "INVITE_HASH_EXPIRED"], "channels.exportMessageLink": ["CHANNEL_INVALID"], "channels.getAdminLog": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.getChannels": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getFullChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "MESSAGE_IDS_EMPTY"], "channels.getParticipant": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_ID_INVALID", "USER_NOT_PARTICIPANT"], "channels.getParticipants": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_INVALID"], "channels.joinChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHANNELS_TOO_MUCH"], "channels.leaveChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "USER_CREATOR", "USER_NOT_PARTICIPANT"], "channels.readHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.readMessageContents": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.reportSpam": ["CHANNEL_INVALID"], "channels.setStickers": ["CHANNEL_INVALID", "PARTICIPANTS_TOO_FEW"], "channels.toggleInvites": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.toggleSignatures": ["CHANNEL_INVALID"], "channels.updatePinnedMessage": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.updateUsername": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USERNAME_INVALID", "USERNAME_OCCUPIED"], "messages.editMessage": ["CHANNEL_INVALID", "MESSAGE_EDIT_TIME_EXPIRED", "MESSAGE_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED", "PEER_ID_INVALID"], "messages.forwardMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "MEDIA_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_IDS_EMPTY", "PEER_ID_INVALID", "RANDOM_ID_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "messages.getHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerSettings": ["CHANNEL_INVALID", "PEER_ID_INVALID"], "messages.sendMedia": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "FILE_PART_0_MISSING", "FILE_PART_LENGTH_INVALID", "FILE_PARTS_INVALID", "INPUT_USER_DEACTIVATED", "MEDIA_CAPTION_TOO_LONG", "MEDIA_EMPTY", "PEER_ID_INVALID", "PHOTO_EXT_INVALID", "USER_IS_BLOCKED", "USER_IS_BOT", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "messages.setTyping": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "updates.getChannelDifference": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID", "RANGES_INVALID"], "messages.getMessagesViews": ["CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerDialogs": ["CHANNEL_PRIVATE", "PEER_ID_INVALID"], "messages.addChatUser": ["CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_ALREADY_PARTICIPANT", "USER_ID_INVALID", "USERS_TOO_MUCH"], "messages.discardEncryption": ["CHAT_ID_EMPTY", "ENCRYPTION_ALREADY_DECLINED", "ENCRYPTION_ID_INVALID"], "messages.acceptEncryption": ["CHAT_ID_INVALID", "ENCRYPTION_ALREADY_ACCEPTED", "ENCRYPTION_ALREADY_DECLINED"], "messages.deleteChatUser": ["CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_NOT_PARTICIPANT"], "messages.editChatAdmin": ["CHAT_ID_INVALID"], "messages.editChatPhoto": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.editChatTitle": ["CHAT_ID_INVALID"], "messages.exportChatInvite": ["CHAT_ID_INVALID"], "messages.forwardMessage": ["CHAT_ID_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID", "YOU_BLOCKED_USER"], "messages.getChats": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getFullChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.migrateChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.reportEncryptedSpam": ["CHAT_ID_INVALID"], "messages.sendEncrypted": ["CHAT_ID_INVALID", "DATA_INVALID", "MSG_WAIT_FAILED"], "messages.setEncryptedTyping": ["CHAT_ID_INVALID"], "messages.toggleChatAdmins": ["CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "channels.createChannel": ["CHAT_TITLE_EMPTY"], "auth.recoverPassword": ["CODE_EMPTY"], "account.confirmPhone": ["CODE_HASH_INVALID", "PHONE_CODE_EMPTY"], "initConnection": ["CONNECTION_LAYER_INVALID", "INPUT_FETCH_FAIL"], "contacts.block": ["CONTACT_ID_INVALID"], "contacts.deleteContact": ["CONTACT_ID_INVALID"], "contacts.unblock": ["CONTACT_ID_INVALID"], "messages.getBotCallbackAnswer": ["DATA_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID"], "messages.sendEncryptedService": ["DATA_INVALID", "MSG_WAIT_FAILED"], "auth.exportAuthorization": ["DC_ID_INVALID"], "messages.requestEncryption": ["DH_G_A_INVALID", "USER_ID_INVALID"], "auth.bindTempAuthKey": ["ENCRYPTED_MESSAGE_INVALID", "INPUT_REQUEST_TOO_LONG", "TEMP_AUTH_KEY_EMPTY"], "messages.setBotPrecheckoutResults": ["ERROR_TEXT_EMPTY"], "contacts.importCard": ["EXPORT_CARD_INVALID"], "upload.getFile": ["FILE_ID_INVALID", "LIMIT_INVALID", "LOCATION_INVALID", "OFFSET_INVALID"], "photos.uploadProfilePhoto": ["FILE_PART_0_MISSING", "FILE_PARTS_INVALID", "IMAGE_PROCESS_FAILED", "PHOTO_CROP_SIZE_SMALL", "PHOTO_EXT_INVALID"], "upload.saveBigFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "FILE_PART_SIZE_INVALID", "FILE_PARTS_INVALID"], "upload.saveFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID"], "auth.signUp": ["FIRSTNAME_INVALID", "PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_OCCUPIED"], "messages.saveGif": ["GIF_ID_INVALID"], "account.resetAuthorization": ["HASH_INVALID"], "account.sendConfirmPhoneCode": ["HASH_INVALID"], "messages.sendInlineBotResult": ["INLINE_RESULT_EXPIRED", "PEER_ID_INVALID", "QUERY_ID_EMPTY"], "messages.getDialogs": ["INPUT_CONSTRUCTOR_INVALID", "OFFSET_PEER_ID_INVALID"], "messages.search": ["INPUT_CONSTRUCTOR_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "PEER_ID_NOT_SUPPORTED", "SEARCH_QUERY_EMPTY", "USER_ID_INVALID"], "messages.checkChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID"], "messages.importChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID", "USER_ALREADY_PARTICIPANT", "USERS_TOO_MUCH"], "{}": ["INVITE_HASH_EXPIRED"], "langpack.getDifference": ["LANG_PACK_INVALID"], "langpack.getLangPack": ["LANG_PACK_INVALID"], "langpack.getLanguages": ["LANG_PACK_INVALID"], "langpack.getStrings": ["LANG_PACK_INVALID"], "upload.getWebFile": ["LOCATION_INVALID"], "photos.getUserPhotos": ["MAX_ID_INVALID", "USER_ID_INVALID"], "auth.sendInvites": ["MESSAGE_EMPTY"], "messages.editInlineBotMessage": ["MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED"], "messages.getInlineGameHighScores": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.setInlineGameScore": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "payments.getPaymentForm": ["MESSAGE_ID_INVALID"], "payments.getPaymentReceipt": ["MESSAGE_ID_INVALID"], "payments.sendPaymentForm": ["MESSAGE_ID_INVALID"], "payments.validateRequestedInfo": ["MESSAGE_ID_INVALID"], "messages.readEncryptedHistory": ["MSG_WAIT_FAILED"], "messages.receivedQueue": ["MSG_WAIT_FAILED"], "messages.sendEncryptedFile": ["MSG_WAIT_FAILED"], "account.updatePasswordSettings": ["NEW_SALT_INVALID", "NEW_SETTINGS_INVALID", "PASSWORD_HASH_INVALID"], "auth.requestPasswordRecovery": ["PASSWORD_EMPTY"], "account.getPasswordSettings": ["PASSWORD_HASH_INVALID"], "account.getTmpPassword": ["PASSWORD_HASH_INVALID", "TMP_PASSWORD_DISABLED"], "auth.checkPassword": ["PASSWORD_HASH_INVALID"], "account.getNotifySettings": ["PEER_ID_INVALID"], "account.reportPeer": ["PEER_ID_INVALID"], "account.updateNotifySettings": ["PEER_ID_INVALID"], "contacts.resetTopPeerRating": ["PEER_ID_INVALID"], "messages.deleteHistory": ["PEER_ID_INVALID"], "messages.getGameHighScores": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getMessageEditData": ["PEER_ID_INVALID"], "messages.getUnreadMentions": ["PEER_ID_INVALID"], "messages.hideReportSpam": ["PEER_ID_INVALID"], "messages.readHistory": ["PEER_ID_INVALID"], "messages.reorderPinnedDialogs": ["PEER_ID_INVALID"], "messages.reportSpam": ["PEER_ID_INVALID"], "messages.saveDraft": ["PEER_ID_INVALID"], "messages.sendScreenshotNotification": ["PEER_ID_INVALID"], "messages.setGameScore": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.toggleDialogPin": ["PEER_ID_INVALID"], "auth.signIn": ["PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_UNOCCUPIED"], "auth.checkPhone": ["PHONE_NUMBER_BANNED", "PHONE_NUMBER_INVALID"], "account.changePhone": ["PHONE_NUMBER_INVALID"], "account.sendChangePhoneCode": ["PHONE_NUMBER_INVALID"], "auth.cancelCode": ["PHONE_NUMBER_INVALID"], "auth.resendCode": ["PHONE_NUMBER_INVALID"], "account.getPrivacy": ["PRIVACY_KEY_INVALID"], "account.setPrivacy": ["PRIVACY_KEY_INVALID"], "bots.answerWebhookJSONQuery": ["QUERY_ID_INVALID", "USER_BOT_INVALID"], "messages.setBotCallbackAnswer": ["QUERY_ID_INVALID"], "messages.setBotShippingResults": ["QUERY_ID_INVALID"], "contacts.search": ["QUERY_TOO_SHORT", "SEARCH_QUERY_EMPTY"], "messages.getDhConfig": ["RANDOM_LENGTH_INVALID"], "upload.reuploadCdnFile": ["RSA_DECRYPT_FAILED"], "messages.searchGifs": ["SEARCH_QUERY_EMPTY"], "messages.searchGlobal": ["SEARCH_QUERY_EMPTY"], "messages.getDocumentByHash": ["SHA256_HASH_INVALID"], "messages.faveSticker": ["STICKER_ID_INVALID"], "messages.saveRecentSticker": ["STICKER_ID_INVALID"], "messages.getStickerSet": ["STICKERSET_INVALID"], "messages.installStickerSet": ["STICKERSET_INVALID"], "messages.uninstallStickerSet": ["STICKERSET_INVALID"], "account.registerDevice": ["TOKEN_INVALID"], "account.unregisterDevice": ["TOKEN_INVALID"], "account.setAccountTTL": ["TTL_DAYS_INVALID"], "contacts.getTopPeers": ["TYPES_EMPTY"], "bots.sendCustomRequest": ["USER_BOT_INVALID"], "messages.getCommonChats": ["USER_ID_INVALID"], "users.getFullUser": ["USER_ID_INVALID"], "account.checkUsername": ["USERNAME_INVALID"], "account.updateUsername": ["USERNAME_INVALID", "USERNAME_NOT_MODIFIED", "USERNAME_OCCUPIED"], "contacts.resolveUsername": ["USERNAME_INVALID", "USERNAME_NOT_OCCUPIED"], "messages.createChat": ["USERS_TOO_FEW"]}, "401": {"contacts.resolveUsername": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "messages.getHistory": ["AUTH_KEY_PERM_EMPTY"], "auth.signIn": ["SESSION_PASSWORD_NEEDED"], "messages.getDialogs": ["SESSION_PASSWORD_NEEDED"], "updates.getDifference": ["SESSION_PASSWORD_NEEDED"], "updates.getState": ["SESSION_PASSWORD_NEEDED"], "upload.saveFilePart": ["SESSION_PASSWORD_NEEDED"], "users.getUsers": ["SESSION_PASSWORD_NEEDED"]}, "500": {"auth.sendCode": ["AUTH_RESTART"], "phone.acceptCall": ["CALL_OCCUPY_FAILED"], "messages.acceptEncryption": ["ENCRYPTION_OCCUPY_FAILED"], "updates.getChannelDifference": ["HISTORY_GET_FAILED", "PERSISTENT_TIMESTAMP_OUTDATED"], "users.getUsers": ["MEMBER_NO_LOCATION", "NEED_MEMBER_INVALID"], "auth.signUp": ["MEMBER_OCCUPY_PRIMARY_LOC_FAILED", "REG_ID_GENERATE_FAILED"], "channels.getChannels": ["NEED_CHAT_INVALID"], "messages.editChatTitle": ["NEED_CHAT_INVALID"], "contacts.deleteContacts": ["NEED_MEMBER_INVALID"], "contacts.importCard": ["NEED_MEMBER_INVALID"], "updates.getDifference": ["NEED_MEMBER_INVALID"], "phone.requestCall": ["PARTICIPANT_CALL_FAILED"], "messages.forwardMessages": ["PTS_CHANGE_EMPTY", "RANDOM_ID_DUPLICATE"], "messages.sendMessage": ["RANDOM_ID_DUPLICATE"], "messages.sendMedia": ["STORAGE_CHECK_FAILED"], "upload.getCdnFile": ["UNKNOWN_METHOD"]}, "403": {"channels.getFullChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "updates.getChannelDifference": ["CHANNEL_PUBLIC_GROUP_NA"], "channels.editAdmin": ["CHAT_ADMIN_INVITE_REQUIRED", "RIGHT_FORBIDDEN", "USER_PRIVACY_RESTRICTED"], "messages.migrateChat": ["CHAT_ADMIN_REQUIRED"], "messages.forwardMessages": ["CHAT_SEND_GIFS_FORBIDDEN", "CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_SEND_STICKERS_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "channels.inviteToChannel": ["CHAT_WRITE_FORBIDDEN", "USER_CHANNELS_TOO_MUCH", "USER_PRIVACY_RESTRICTED"], "messages.editMessage": ["CHAT_WRITE_FORBIDDEN", "MESSAGE_AUTHOR_REQUIRED"], "messages.sendMedia": ["CHAT_WRITE_FORBIDDEN"], "messages.sendMessage": ["CHAT_WRITE_FORBIDDEN"], "messages.setTyping": ["CHAT_WRITE_FORBIDDEN"], "messages.getMessageEditData": ["MESSAGE_AUTHOR_REQUIRED"], "channels.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.setInlineBotResults": ["USER_BOT_INVALID"], "phone.requestCall": ["USER_IS_BLOCKED", "USER_PRIVACY_RESTRICTED"], "messages.addChatUser": ["USER_NOT_MUTUAL_CONTACT", "USER_PRIVACY_RESTRICTED"], "channels.createChannel": ["USER_RESTRICTED"], "messages.createChat": ["USER_RESTRICTED"]}, "406": {"auth.checkPhone": ["PHONE_NUMBER_INVALID"], "auth.sendCode": ["PHONE_PASSWORD_FLOOD"]}, "-503": {"auth.resetAuthorizations": ["Timeout"], "contacts.deleteContacts": ["Timeout"], "messages.forwardMessages": ["Timeout"], "messages.getBotCallbackAnswer": ["Timeout"], "messages.getHistory": ["Timeout"], "messages.getInlineBotResults": ["Timeout"], "updates.getState": ["Timeout"]}}, "human_result": {"-429": ["Too many requests"], "ABOUT_TOO_LONG": ["The provided bio is too long"], "ACCESS_TOKEN_EXPIRED": ["Bot token expired"], "ACCESS_TOKEN_INVALID": ["The provided token is not valid"], "ACTIVE_USER_REQUIRED": ["The method is only available to already activated users"], "API_ID_INVALID": ["The api_id/api_hash combination is invalid"], "ARTICLE_TITLE_EMPTY": ["The title of the article is empty"], "AUTH_BYTES_INVALID": ["The provided authorization is invalid"], "AUTH_KEY_PERM_EMPTY": ["The temporary auth key must be binded to the permanent auth key to use these methods."], "AUTH_KEY_UNREGISTERED": ["The authorization key has expired"], "AUTH_RESTART": ["Restart the authorization process"], "BOT_GROUPS_BLOCKED": ["This bot can't be added to groups"], "BOT_INLINE_DISABLED": ["This bot can't be used in inline mode"], "BOT_INVALID": ["This is not a valid bot"], "BOT_METHOD_INVALID": ["This method cannot be run by a bot"], "BOT_MISSING": ["This method can only be run by a bot"], "BOTS_TOO_MUCH": ["There are too many bots in this chat/channel"], "BUTTON_DATA_INVALID": ["The provided button data is invalid"], "BUTTON_TYPE_INVALID": ["The type of one of the buttons you provided is invalid"], "BUTTON_URL_INVALID": ["Button URL invalid"], "CALL_ALREADY_ACCEPTED": ["The call was already accepted"], "CALL_ALREADY_DECLINED": ["The call was already declined"], "CALL_OCCUPY_FAILED": ["The call failed because the user is already making another call"], "CALL_PEER_INVALID": ["The provided call peer object is invalid"], "CALL_PROTOCOL_FLAGS_INVALID": ["Call protocol flags invalid"], "CDN_METHOD_INVALID": ["You can't call this method in a CDN DC"], "CHANNEL_INVALID": ["The provided channel is invalid"], "CHANNEL_PRIVATE": ["You haven't joined this channel/supergroup"], "CHANNEL_PUBLIC_GROUP_NA": ["channel/supergroup not available"], "CHANNELS_TOO_MUCH": ["You have joined too many channels/supergroups"], "CHAT_ABOUT_NOT_MODIFIED": ["About text has not changed"], "CHAT_ADMIN_INVITE_REQUIRED": ["You do not have the rights to do this"], "CHAT_ADMIN_REQUIRED": ["You must be an admin in this chat to do this"], "CHAT_FORBIDDEN": ["You cannot write in this chat"], "CHAT_ID_EMPTY": ["The provided chat ID is empty"], "CHAT_ID_INVALID": ["The provided chat id is invalid"], "CHAT_NOT_MODIFIED": ["The pinned message wasn't modified"], "CHAT_SEND_GIFS_FORBIDDEN": ["You can't send gifs in this chat"], "CHAT_SEND_MEDIA_FORBIDDEN": ["You can't send media in this chat"], "CHAT_SEND_STICKERS_FORBIDDEN": ["You can't send stickers in this chat."], "CHAT_TITLE_EMPTY": ["No chat title provided"], "CHAT_WRITE_FORBIDDEN": ["You can't write in this chat"], "CODE_EMPTY": ["The provided code is empty"], "CODE_HASH_INVALID": ["Code hash invalid"], "CONNECTION_API_ID_INVALID": ["The provided API id is invalid"], "CONNECTION_LANG_PACK_INVALID": ["Language pack invalid"], "CONNECTION_LAYER_INVALID": ["Layer invalid"], "CONTACT_ID_INVALID": ["The provided contact ID is invalid"], "DATA_INVALID": ["Encrypted data invalid"], "DATA_JSON_INVALID": ["The provided JSON data is invalid"], "DATE_EMPTY": ["Date empty"], "DC_ID_INVALID": ["The provided DC ID is invalid"], "DH_G_A_INVALID": ["g_a invalid"], "ENCRYPTED_MESSAGE_INVALID": ["Encrypted message invalid"], "ENCRYPTION_ALREADY_ACCEPTED": ["Secret chat already accepted"], "ENCRYPTION_ALREADY_DECLINED": ["The secret chat was already declined"], "ENCRYPTION_ID_INVALID": ["The provided secret chat ID is invalid"], "ENCRYPTION_OCCUPY_FAILED": ["Internal server error while accepting secret chat"], "ENTITY_MENTION_USER_INVALID": ["You can't use this entity"], "ERROR_TEXT_EMPTY": ["The provided error message is empty"], "EXPORT_CARD_INVALID": ["Provided card is invalid"], "FIELD_NAME_EMPTY": ["The field with the name FIELD_NAME is missing"], "FIELD_NAME_INVALID": ["The field with the name FIELD_NAME is invalid"], "FILE_ID_INVALID": ["The provided file id is invalid"], "FILE_PART_0_MISSING": ["File part 0 missing"], "FILE_PART_EMPTY": ["The provided file part is empty"], "FILE_PART_INVALID": ["The file part number is invalid"], "FILE_PART_LENGTH_INVALID": ["The length of a file part is invalid"], "FILE_PART_SIZE_INVALID": ["The provided file part size is invalid"], "FILE_PARTS_INVALID": ["The number of file parts is invalid"], "FIRSTNAME_INVALID": ["The first name is invalid"], "FLOOD_WAIT_666": ["Spooky af m8"], "GIF_ID_INVALID": ["The provided GIF ID is invalid"], "HASH_INVALID": ["The provided hash is invalid"], "HISTORY_GET_FAILED": ["Fetching of history failed"], "IMAGE_PROCESS_FAILED": ["Failure while processing image"], "INLINE_RESULT_EXPIRED": ["The inline query expired"], "INPUT_CONSTRUCTOR_INVALID": ["The provided constructor is invalid"], "INPUT_FETCH_ERROR": ["An error occurred while deserializing TL parameters"], "INPUT_FETCH_FAIL": ["Failed deserializing TL payload"], "INPUT_LAYER_INVALID": ["The provided layer is invalid"], "INPUT_METHOD_INVALID": ["The provided method is invalid"], "INPUT_REQUEST_TOO_LONG": ["The request is too big"], "INPUT_USER_DEACTIVATED": ["The specified user was deleted"], "INTERDC_1_CALL_ERROR": ["An error occurred while communicating with DC 1"], "INTERDC_1_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 1"], "INTERDC_2_CALL_ERROR": ["An error occurred while communicating with DC 2"], "INTERDC_2_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 2"], "INTERDC_3_CALL_ERROR": ["An error occurred while communicating with DC 3"], "INTERDC_3_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 3"], "INTERDC_4_CALL_ERROR": ["An error occurred while communicating with DC 4"], "INTERDC_4_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 4"], "INTERDC_5_CALL_ERROR": ["An error occurred while communicating with DC 5"], "INTERDC_5_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 5"], "INVITE_HASH_EMPTY": ["The invite hash is empty"], "INVITE_HASH_EXPIRED": ["The invite link has expired"], "INVITE_HASH_INVALID": ["The invite hash is invalid"], "LANG_PACK_INVALID": ["The provided language pack is invalid"], "LASTNAME_INVALID": ["The last name is invalid"], "LIMIT_INVALID": ["The provided limit is invalid"], "LOCATION_INVALID": ["The provided location is invalid"], "MAX_ID_INVALID": ["The provided max ID is invalid"], "MD5_CHECKSUM_INVALID": ["The MD5 checksums do not match"], "MEDIA_CAPTION_TOO_LONG": ["The caption is too long"], "MEDIA_EMPTY": ["The provided media object is invalid"], "MEMBER_OCCUPY_PRIMARY_LOC_FAILED": ["Occupation of primary member location failed"], "MESSAGE_AUTHOR_REQUIRED": ["Message author required"], "MESSAGE_DELETE_FORBIDDEN": ["You can't delete one of the messages you tried to delete, most likely because it is a service message."], "MESSAGE_EDIT_TIME_EXPIRED": ["You can't edit this message anymore, too much time has passed since its creation."], "MESSAGE_EMPTY": ["The provided message is empty"], "MESSAGE_ID_INVALID": ["The provided message id is invalid"], "MESSAGE_IDS_EMPTY": ["No message ids were provided"], "MESSAGE_NOT_MODIFIED": ["The message text has not changed"], "MESSAGE_TOO_LONG": ["The provided message is too long"], "MSG_WAIT_FAILED": ["A waiting call returned an error"], "NEED_CHAT_INVALID": ["The provided chat is invalid"], "NEED_MEMBER_INVALID": ["The provided member is invalid"], "NEW_SALT_INVALID": ["The new salt is invalid"], "NEW_SETTINGS_INVALID": ["The new settings are invalid"], "OFFSET_INVALID": ["The provided offset is invalid"], "OFFSET_PEER_ID_INVALID": ["The provided offset peer is invalid"], "PACK_SHORT_NAME_INVALID": ["Short pack name invalid"], "PARTICIPANT_CALL_FAILED": ["Failure while making call"], "PARTICIPANT_VERSION_OUTDATED": ["The other participant does not use an up to date telegram client with support for calls"], "PARTICIPANTS_TOO_FEW": ["Not enough participants"], "PASSWORD_EMPTY": ["The provided password is empty"], "PASSWORD_HASH_INVALID": ["The provided password hash is invalid"], "PEER_FLOOD": ["Too many requests"], "PEER_ID_INVALID": ["The provided peer id is invalid"], "PEER_ID_NOT_SUPPORTED": ["The provided peer ID is not supported"], "PERSISTENT_TIMESTAMP_EMPTY": ["Persistent timestamp empty"], "PERSISTENT_TIMESTAMP_INVALID": ["Persistent timestamp invalid"], "PERSISTENT_TIMESTAMP_OUTDATED": ["Persistent timestamp outdated"], "PHONE_CODE_EMPTY": ["phone_code is missing"], "PHONE_CODE_EXPIRED": ["The phone code you provided has expired, this may happen if it was sent to any chat on telegram (if the code is sent through a telegram chat (not the official account) to avoid it append or prepend to the code some chars)"], "PHONE_CODE_HASH_EMPTY": ["phone_code_hash is missing"], "PHONE_CODE_INVALID": ["The provided phone code is invalid"], "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN": [""], "PHONE_NUMBER_BANNED": ["The provided phone number is banned from telegram"], "PHONE_NUMBER_FLOOD": ["You asked for the code too many times."], "PHONE_NUMBER_INVALID": ["The phone number is invalid"], "PHONE_NUMBER_OCCUPIED": ["The phone number is already in use"], "PHONE_NUMBER_UNOCCUPIED": ["The phone number is not yet being used"], "PHONE_PASSWORD_FLOOD": ["You have tried logging in too many times"], "PHONE_PASSWORD_PROTECTED": ["This phone is password protected"], "PHOTO_CROP_SIZE_SMALL": ["Photo is too small"], "PHOTO_EXT_INVALID": ["The extension of the photo is invalid"], "PHOTO_INVALID_DIMENSIONS": ["The photo dimensions are invalid"], "PRIVACY_KEY_INVALID": ["The privacy key is invalid"], "PTS_CHANGE_EMPTY": ["No PTS change"], "QUERY_ID_EMPTY": ["The query ID is empty"], "QUERY_ID_INVALID": ["The query ID is invalid"], "QUERY_TOO_SHORT": ["The query string is too short"], "RANDOM_ID_DUPLICATE": ["You provided a random ID that was already used"], "RANDOM_ID_INVALID": ["A provided random ID is invalid"], "RANDOM_LENGTH_INVALID": ["Random length invalid"], "RANGES_INVALID": ["Invalid range provided"], "REG_ID_GENERATE_FAILED": ["Failure while generating registration ID"], "REPLY_MARKUP_INVALID": ["The provided reply markup is invalid"], "RESULT_TYPE_INVALID": ["Result type invalid"], "RIGHT_FORBIDDEN": ["Your admin rights do not allow you to do this"], "RPC_CALL_FAIL": ["Telegram is having internal issues, please try again later."], "RPC_MCGET_FAIL": ["Telegram is having internal issues, please try again later."], "RSA_DECRYPT_FAILED": ["Internal RSA decryption failed"], "SEARCH_QUERY_EMPTY": ["The search query is empty"], "SEND_MESSAGE_TYPE_INVALID": ["The message type is invalid"], "SESSION_PASSWORD_NEEDED": ["2FA is enabled, use a password to login"], "SHA256_HASH_INVALID": ["The provided SHA256 hash is invalid"], "START_PARAM_EMPTY": ["The start parameter is empty"], "START_PARAM_INVALID": ["Start parameter invalid"], "STICKER_ID_INVALID": ["The provided sticker ID is invalid"], "STICKER_INVALID": ["The provided sticker is invalid"], "STICKERS_EMPTY": ["No sticker provided"], "STICKERSET_INVALID": ["The provided sticker set is invalid"], "STORAGE_CHECK_FAILED": ["Server storage check failed"], "TEMP_AUTH_KEY_EMPTY": ["No temporary auth key provided"], "Timeout": ["A timeout occurred while fetching data from the bot"], "TMP_PASSWORD_DISABLED": ["The temporary password is disabled"], "TOKEN_INVALID": ["The provided token is invalid"], "TTL_DAYS_INVALID": ["The provided TTL is invalid"], "TYPE_CONSTRUCTOR_INVALID": ["The type constructor is invalid"], "TYPES_EMPTY": ["The types field is empty"], "UNKNOWN_METHOD": ["The method you tried to call cannot be called on non-CDN DCs"], "USER_ADMIN_INVALID": ["You're not an admin"], "USER_ALREADY_PARTICIPANT": ["The user is already in the group"], "USER_BANNED_IN_CHANNEL": ["You're banned from sending messages in supergroups/channels"], "USER_BLOCKED": ["User blocked"], "USER_BOT": ["Bots can only be admins in channels."], "USER_BOT_INVALID": ["This method can only be called by a bot"], "USER_BOT_REQUIRED": ["This method can only be called by a bot"], "USER_CHANNELS_TOO_MUCH": ["One of the users you tried to add is already in too many channels/supergroups"], "USER_CREATOR": ["You can't leave this channel, because you're its creator"], "USER_DEACTIVATED": ["The user was deactivated"], "USER_ID_INVALID": ["The provided user ID is invalid"], "USER_IS_BLOCKED": ["User is blocked"], "USER_IS_BOT": ["Bots can't send messages to other bots"], "USER_KICKED": ["This user was kicked from this supergroup/channel"], "USER_NOT_MUTUAL_CONTACT": ["The provided user is not a mutual contact"], "USER_NOT_PARTICIPANT": ["You're not a member of this supergroup/channel"], "USER_PRIVACY_RESTRICTED": ["The user's privacy settings do not allow you to do this"], "USER_RESTRICTED": ["You're spamreported, you can't create channels or chats."], "USERNAME_INVALID": ["The provided username is not valid"], "USERNAME_NOT_MODIFIED": ["The username was not modified"], "USERNAME_NOT_OCCUPIED": ["The provided username is not occupied"], "USERNAME_OCCUPIED": ["The provided username is already occupied"], "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)"], "WEBPAGE_CURL_FAILED": ["Failure while fetching the webpage with cURL"], "YOU_BLOCKED_USER": ["You blocked this user"]}} \ No newline at end of file diff --git a/telethon_generator/parser/tl_object.py b/telethon_generator/parser/tl_object.py index 416bc587..79b4385d 100644 --- a/telethon_generator/parser/tl_object.py +++ b/telethon_generator/parser/tl_object.py @@ -98,12 +98,17 @@ class TLObject: def class_name(self): """Gets the class name following the Python style guidelines""" + return self.class_name_for(self.name, self.is_function) + @staticmethod + def class_name_for(typename, is_function=False): + """Gets the class name following the Python style guidelines""" # Courtesy of http://stackoverflow.com/a/31531797/4759433 - result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), self.name) + result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), + typename) result = result[:1].upper() + result[1:].replace('_', '') # If it's a function, let it end with "Request" to identify them - if self.is_function: + if is_function: result += 'Request' return result @@ -192,6 +197,7 @@ class TLArg: # Default values self.is_vector = False self.is_flag = False + self.skip_constructor_id = False self.flag_index = -1 # Special case: some types can be inferred, which makes it @@ -222,7 +228,7 @@ class TLArg: self.type = flag_match.group(2) # Then check if the type is a Vector - vector_match = re.match(r'vector<(\w+)>', self.type, re.IGNORECASE) + vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type) if vector_match: self.is_vector = True @@ -234,6 +240,11 @@ class TLArg: # Update the type to match the one inside the vector self.type = vector_match.group(1) + # See use_vector_id. An example of such case is ipPort in + # help.configSpecial + if self.type.split('.')[-1][0].islower(): + self.skip_constructor_id = True + # 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. # Note that this is not a valid Telegram object, but it's easier to work with diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index e76dffaa..8fc6bb2d 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -5,7 +5,7 @@ import struct from zlib import crc32 from collections import defaultdict -from .parser import SourceBuilder, TLParser +from .parser import SourceBuilder, TLParser, TLObject AUTO_GEN_NOTICE = \ '"""File generated by TLObjects\' generator. All changes will be ERASED"""' @@ -129,6 +129,9 @@ class TLGenerator: builder.writeln( 'from {}.tl.tlobject import TLObject'.format('.' * depth) ) + builder.writeln( + 'from {}.tl import types'.format('.' * depth) + ) # Add the relative imports to the namespaces, # unless we already are in a namespace. @@ -143,7 +146,7 @@ class TLGenerator: builder.writeln( 'from {}.utils import get_input_peer, ' 'get_input_channel, get_input_user, ' - 'get_input_media'.format('.' * depth) + 'get_input_media, get_input_photo'.format('.' * depth) ) # Import 'os' for those needing access to 'os.urandom()' @@ -151,7 +154,7 @@ class TLGenerator: # for all those TLObjects with arg.can_be_inferred. builder.writeln('import os') - # Import struct for the .to_bytes(self) serialization + # Import struct for the .__bytes__(self) serialization builder.writeln('import struct') # Generate the class for every TLObject @@ -299,8 +302,8 @@ class TLGenerator: builder.end_block() - # Write the .to_bytes() function - builder.writeln('def to_bytes(self):') + # Write the .__bytes__() function + builder.writeln('def __bytes__(self):') # Some objects require more than one flag parameter to be set # at the same time. In this case, add an assertion. @@ -311,11 +314,11 @@ class TLGenerator: for ra in repeated_args.values(): if len(ra) > 1: - cnd1 = ('self.{} is None'.format(a.name) for a in ra) - cnd2 = ('self.{} is not None'.format(a.name) for a in ra) + cnd1 = ('self.{}'.format(a.name) for a in ra) + cnd2 = ('not self.{}'.format(a.name) for a in ra) builder.writeln( "assert ({}) or ({}), '{} parameters must all " - "be None or neither be None'".format( + "be False-y (like None) or all me True-y'".format( ' and '.join(cnd1), ' and '.join(cnd2), ', '.join(a.name for a in ra) ) @@ -335,32 +338,28 @@ class TLGenerator: builder.writeln('))') builder.end_block() - # Write the empty() function, which returns an "empty" - # instance, in which all attributes are set to None + # Write the static from_reader(reader) function builder.writeln('@staticmethod') - builder.writeln('def empty():') + builder.writeln('def from_reader(reader):') + for arg in tlobject.args: + TLGenerator.write_read_code( + builder, arg, tlobject.args, name='_' + arg.name + ) + builder.writeln('return {}({})'.format( - tlobject.class_name(), ', '.join('None' for _ in range(len(args))) + tlobject.class_name(), ', '.join( + '{0}=_{0}'.format(a.name) for a in tlobject.sorted_args() + if not a.flag_indicator and not a.generic_definition + ) )) builder.end_block() - # Write the on_response(self, reader) function - builder.writeln('def on_response(self, reader):') - # Do not read constructor's ID, since - # that's already been read somewhere else + # Only requests can have a different response that's not their + # serialized body, that is, we'll be setting their .result. if tlobject.is_function: + builder.writeln('def on_response(self, reader):') TLGenerator.write_request_result_code(builder, tlobject) - else: - if tlobject.args: - for arg in 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') - builder.end_block() + builder.end_block() # Write the __str__(self) and stringify(self) functions builder.writeln('def __str__(self):') @@ -406,6 +405,8 @@ class TLGenerator: TLGenerator.write_get_input(builder, arg, 'get_input_user') elif arg.type == 'InputMedia' and tlobject.is_function: TLGenerator.write_get_input(builder, arg, 'get_input_media') + elif arg.type == 'InputPhoto' and tlobject.is_function: + TLGenerator.write_get_input(builder, arg, 'get_input_photo') else: builder.writeln('self.{0} = {0}'.format(arg.name)) @@ -440,10 +441,10 @@ class TLGenerator: @staticmethod def write_to_bytes(builder, arg, args, name=None): """ - Writes the .to_bytes() code for the given argument + Writes the .__bytes__() code for the given argument :param builder: The source code builder :param arg: The argument to write - :param args: All the other arguments in TLObject same to_bytes. + :param args: All the other arguments in TLObject same __bytes__. This is required to determine the flags value :param name: The name of the argument. Defaults to "self.argname" This argument is an option because it's required when @@ -539,7 +540,7 @@ class TLGenerator: else: # Else it may be a custom type - builder.write('{}.to_bytes()'.format(name)) + builder.write('bytes({})'.format(name)) if arg.is_flag: builder.write(')') @@ -549,9 +550,10 @@ class TLGenerator: return True # Something was written @staticmethod - def write_onresponse_code(builder, arg, args, name=None): + def write_read_code(builder, arg, args, name): """ - Writes the receive code for the given argument + Writes the read code for the given argument, setting the + arg.name variable to its read value. :param builder: The source code builder :param arg: The argument to write @@ -565,12 +567,17 @@ class TLGenerator: if arg.generic_definition: return # Do nothing, this only specifies a later type - if name is None: - name = 'self.{}'.format(arg.name) - # The argument may be a flag, only write that flag was given! was_flag = False if arg.is_flag: + # Treat 'true' flags as a special case, since they're true if + # they're set, and nothing else needs to actually be read. + if 'true' == arg.type: + builder.writeln( + '{} = bool(flags & {})'.format(name, 1 << arg.flag_index) + ) + return + was_flag = True builder.writeln('if flags & {}:'.format( 1 << arg.flag_index @@ -585,11 +592,10 @@ class TLGenerator: builder.writeln("reader.read_int()") builder.writeln('{} = []'.format(name)) - builder.writeln('_len = reader.read_int()') - builder.writeln('for _ in range(_len):') + builder.writeln('for _ in range(reader.read_int()):') # Temporary disable .is_vector, not to enter this if again arg.is_vector = False - TLGenerator.write_onresponse_code(builder, arg, args, name='_x') + TLGenerator.write_read_code(builder, arg, args, name='_x') builder.writeln('{}.append(_x)'.format(name)) arg.is_vector = True @@ -635,14 +641,21 @@ class TLGenerator: else: # Else it may be a custom type - builder.writeln('{} = reader.tgread_object()'.format(name)) + if not arg.skip_constructor_id: + builder.writeln('{} = reader.tgread_object()'.format(name)) + else: + builder.writeln('{} = types.{}.from_reader(reader)'.format( + name, TLObject.class_name_for(arg.type))) # End vector and flag blocks if required (if we opened them before) if arg.is_vector: builder.end_block() if was_flag: - builder.end_block() + builder.current_indent -= 1 + builder.writeln('else:') + builder.writeln('{} = None'.format(name)) + builder.current_indent -= 1 # Restore .is_flag arg.is_flag = True diff --git a/telethon_tests/crypto_test.py b/telethon_tests/crypto_test.py index cec18084..e11704a4 100644 --- a/telethon_tests/crypto_test.py +++ b/telethon_tests/crypto_test.py @@ -107,17 +107,17 @@ class CryptoTests(unittest.TestCase): @staticmethod def test_generate_key_data_from_nonce(): - server_nonce = b'I am the server nonce.' - new_nonce = b'I am a new calculated nonce.' + server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little') + new_nonce = int.from_bytes(b'The new, calculated 32-bit nonce', byteorder='little') key, iv = utils.generate_key_data_from_nonce(server_nonce, new_nonce) - 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' + expected_key = b'/\xaa\x7f\xa1\xfcs\xef\xa0\x99zh\x03M\xa4\x8e\xb4\xab\x0eE]b\x95|\xfe\xc0\xf8\x1f\xd4\xa0\xd4\xec\x91' + expected_iv = b'\xf7\xae\xe3\xc8+=\xc2\xb8\xd1\xe1\x1b\x0e\x10\x07\x9fn\x9e\xdc\x960\x05\xf9\xea\xee\x8b\xa1h The ' 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 iv == expected_iv, 'IV ("{}") does not equal expected ("{}")'.format( + iv, expected_iv) @staticmethod def test_fingerprint_from_key():