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/network/authenticator.py b/telethon/network/authenticator.py index 766a4c00..be5aa76a 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -42,7 +42,7 @@ async def _do_authentication(connection): req_pq_request = ReqPqRequest( nonce=int.from_bytes(os.urandom(16), 'big', signed=True) ) - await sender.send(req_pq_request.to_bytes()) + await sender.send(bytes(req_pq_request)) with BinaryReader(await sender.receive()) as reader: req_pq_request.on_response(reader) @@ -60,12 +60,12 @@ async 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 @@ async def _do_authentication(connection): public_key_fingerprint=target_fingerprint, encrypted_data=cipher_text ) - await sender.send(req_dh_params.to_bytes()) + await sender.send(bytes(req_dh_params)) # Step 2 response: DH Exchange with BinaryReader(await sender.receive()) as reader: @@ -138,12 +138,12 @@ async 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 @@ -156,7 +156,7 @@ async def _do_authentication(connection): server_nonce=res_pq.server_nonce, encrypted_data=client_dh_encrypted, ) - await sender.send(set_client_dh.to_bytes()) + await sender.send(bytes(set_client_dh)) # Step 3 response: Complete DH Exchange with BinaryReader(await sender.receive()) as reader: diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 1684230d..a71df5e5 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -39,7 +39,7 @@ class MtProtoSender: self._logger = logging.getLogger(__name__) # Message IDs that need confirmation - self._need_confirmation = [] + self._need_confirmation = set() # Requests (as msg_id: Message) sent waiting to be received self._pending_receive = {} @@ -74,7 +74,7 @@ class MtProtoSender: # Pack everything in the same container if we need to send AckRequests if self._need_confirmation: messages.append( - TLMessage(self.session, MsgsAck(self._need_confirmation)) + TLMessage(self.session, MsgsAck(list(self._need_confirmation))) ) self._need_confirmation.clear() @@ -125,7 +125,7 @@ class MtProtoSender: plain_text = \ struct.pack(' 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(' - 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 0e4f0013..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. @@ -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) ) @@ -438,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 @@ -537,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(')') @@ -638,7 +641,11 @@ 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: