diff --git a/README.rst b/README.rst index c9f8576c..818f0d57 100755 --- a/README.rst +++ b/README.rst @@ -130,13 +130,20 @@ More examples are also available under the ``telethon_examples/`` folder. Common errors ------------- -Some errors you may encounter when using Telethon can be the ``FloodWaitError``, which tells you -that you've been trying to send the very same request many times, too quickly. You must wait -``flood_wait_error.seconds`` before calling ``client.connect()`` again, since this error also -disconnects the client. +Errors resulting from Telegram queries all subclass the ``RPCError`` class. +This class is further specialized into further errors: -Another common one is the ``RPCError``, which usually has descriptive information on what went wrong. -However, you may encounter something strange. If you don't manage to solve it, please open an issue. +* ``InvalidDCError`` (303), the request must be repeated on another DC. +* ``BadRequestError`` (400), the request contained errors. +* ``UnauthorizedError`` (401), the user is not authorized yet. +* ``ForbiddenError`` (403), privacy violation error. +* ``NotFoundError`` (404), make sure you're invoking ``Request``'s! +* ``FloodError`` (420), the same request was repeated many times. Must wait ``.seconds``. + +Further specialization is also available, for instance, the ``SessionPasswordNeededError`` +when signing in means that a password must be provided to continue. + +If the error is not recognised, it will only be an ``RPCError``. Unless you know what you're doing, you should download media by always using the ``.download_file()`` function, which supports a ``str`` or a file handle as parameters. Otherwise, ``.invoke()`` may raise diff --git a/telethon/__init__.py b/telethon/__init__.py index db442a11..b3971245 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -1,4 +1,3 @@ -from .errors import * from .telegram_bare_client import TelegramBareClient from .telegram_client import TelegramClient from . import tl diff --git a/telethon/errors.py b/telethon/errors.py deleted file mode 100644 index 14ab7d33..00000000 --- a/telethon/errors.py +++ /dev/null @@ -1,266 +0,0 @@ -import re - - -class ReadCancelledError(Exception): - """Occurs when a read operation was cancelled""" - - def __init__(self): - super().__init__(self, 'The read operation was cancelled.') - - -class InvalidParameterError(Exception): - """Occurs when an invalid parameter is given, for example, - when either A or B are required but none is given""" - - -class TypeNotFoundError(Exception): - """Occurs when a type is not found, for example, - when trying to read a TLObject with an invalid constructor code""" - - def __init__(self, invalid_constructor_id): - super().__init__( - self, 'Could not find a matching Constructor ID for the TLObject ' - 'that was supposed to be read with ID {}. Most likely, a TLObject ' - 'was trying to be read when it should not be read.' - .format(hex(invalid_constructor_id))) - - self.invalid_constructor_id = invalid_constructor_id - - -class InvalidDCError(Exception): - def __init__(self, rpc_error): - self.new_dc = rpc_error.__dict__.pop('additional_data') - self.__dict__.update(rpc_error.__dict__) - - -class InvalidChecksumError(Exception): - def __init__(self, checksum, valid_checksum): - super().__init__( - self, - 'Invalid checksum ({} when {} was expected). This packet should be skipped.' - .format(checksum, valid_checksum)) - - self.checksum = checksum - self.valid_checksum = valid_checksum - - -class FloodWaitError(Exception): - def __init__(self, seconds): - super().__init__( - self, - 'Too many requests were made too fast. Must wait {} seconds.' - .format(seconds) - ) - self.seconds = seconds - - -class RPCError(Exception): - - CodeMessages = { - 303: - ('ERROR_SEE_OTHER', - 'The request must be repeated, but directed to a different data center.' - ), - 400: - ('BAD_REQUEST', - 'The query contains errors. In the event that a request was created using a ' - 'form and contains user generated data, the user should be notified that the ' - 'data must be corrected before the query is repeated.'), - 401: - ('UNAUTHORIZED', - 'There was an unauthorized attempt to use functionality available only to ' - 'authorized users.'), - 403: - ('FORBIDDEN', - 'Privacy violation. For example, an attempt to write a message to someone who ' - 'has blacklisted the current user.'), - 404: ('NOT_FOUND', - 'An attempt to invoke a non-existent object, such as a method.'), - 420: - ('FLOOD', - 'The maximum allowed number of attempts to invoke the given method with ' - 'the given input parameters has been exceeded. For example, in an attempt ' - 'to request a large number of text messages (SMS) for the same phone number.' - ), - 500: - ('INTERNAL', - 'An internal server error occurred while a request was being processed; ' - 'for example, there was a disruption while accessing a database or file storage.' - ) - } - - ErrorMessages = { - # 303 ERROR_SEE_OTHER - 'FILE_MIGRATE_(\d+)': - 'The file to be accessed is currently stored in a different data center (#{}).', - 'PHONE_MIGRATE_(\d+)': - 'The phone number a user is trying to use for authorization is associated ' - 'with a different data center (#{}).', - 'NETWORK_MIGRATE_(\d+)': - 'The source IP address is associated with a different data center (#{}, ' - 'for registration).', - 'USER_MIGRATE_(\d+)': - 'The user whose identity is being used to execute queries is associated with ' - 'a different data center (#{} for registration).', - - # 400 BAD_REQUEST - 'FIRSTNAME_INVALID': 'The first name is invalid.', - 'LASTNAME_INVALID': 'The last name is invalid.', - 'PHONE_NUMBER_INVALID': 'The phone number is invalid.', - 'PHONE_CODE_HASH_EMPTY': 'The phone code hash is missing.', - 'PHONE_CODE_EMPTY': 'The phone code is missing.', - 'PHONE_CODE_INVALID': 'The phone code entered was invalid.', - 'PHONE_CODE_EXPIRED': 'The confirmation code has expired.', - 'PHONE_NUMBER_BANNED': - 'The used phone number has been banned from Telegram and cannot ' - 'be used anymore. Possibly check https://www.telegram.org/faq_spam.', - 'API_ID_INVALID': 'The api_id/api_hash combination is invalid.', - 'PHONE_NUMBER_OCCUPIED': 'The phone number is already in use.', - 'PHONE_NUMBER_UNOCCUPIED': 'The phone number is not yet being used.', - 'USERS_TOO_FEW': 'Not enough users (to create a chat, for example).', - 'USERS_TOO_MUCH': - 'The maximum number of users has been exceeded (to create a chat, for example).', - 'TYPE_CONSTRUCTOR_INVALID': 'The type constructor is invalid.', - 'FILE_PART_INVALID': 'The file part number is invalid.', - 'FILE_PARTS_INVALID': 'The number of file parts is invalid.', - 'FILE_PART_(\d+)_MISSING': - 'Part {} of the file is missing from storage.', - 'MD5_CHECKSUM_INVALID': 'The MD5 check-sums do not match.', - 'PHOTO_INVALID_DIMENSIONS': 'The photo dimensions are invalid.', - 'FIELD_NAME_INVALID': 'The field with the name FIELD_NAME is invalid.', - 'FIELD_NAME_EMPTY': 'The field with the name FIELD_NAME is missing.', - 'MSG_WAIT_FAILED': 'A waiting call returned an error.', - 'CHAT_ADMIN_REQUIRED': - 'Chat admin privileges are required to do that in the specified chat ' - '(for example, to send a message in a channel which is not yours).', - 'PASSWORD_HASH_INVALID': - 'The password (and thus its hash value) you entered is invalid.', - 'BOT_METHOD_INVALID': - 'The API access for bot users is restricted. The method you tried ' - 'to invoke cannot be executed as a bot.', - 'PEER_ID_INVALID': - 'An invalid Peer was used. Make sure to pass the right peer type.', - 'MESSAGE_EMPTY': 'Empty or invalid UTF-8 message was sent.', - 'MESSAGE_TOO_LONG': - 'Message was too long. Current maximum length is 4096 UTF-8 characters.', - 'USERNAME_INVALID': - 'Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,32}"', - 'USERNAME_OCCUPIED': 'The username is already taken.', - 'USERNAME_NOT_OCCUPIED': - 'See issue #96 for Telethon - try upgrading the library.', - 'USERNAME_NOT_MODIFIED': - 'The username is not different from the current username', - 'USER_ID_INVALID': - 'Invalid object ID for an user. Make sure to pass the right types.', - 'CHAT_ID_INVALID': - 'Invalid object ID for a chat. Make sure to pass the right types.', - 'CHANNEL_INVALID': - 'Invalid channel object. Make sure to pass the right types.', - 'MESSAGE_ID_INVALID': 'The specified message ID is invalid.', - 'CONNECTION_LAYER_INVALID': - 'The very first request must always be InvokeWithLayerRequest.', - 'INPUT_METHOD_INVALID': - 'The invoked method does not exist anymore or has never existed.', - 'DC_ID_INVALID': - 'This occurs when an authorization is tried to be exported for ' - 'the same data center one is currently connected to.', - - # 401 UNAUTHORIZED - 'AUTH_KEY_UNREGISTERED': 'The key is not registered in the system.', - 'AUTH_KEY_INVALID': 'The key is invalid.', - 'USER_DEACTIVATED': 'The user has been deleted/deactivated.', - 'SESSION_REVOKED': - 'The authorization has been invalidated, because of the user terminating all sessions.', - 'SESSION_EXPIRED': 'The authorization has expired.', - 'ACTIVE_USER_REQUIRED': - 'The method is only available to already activated users.', - 'AUTH_KEY_PERM_EMPTY': - 'The method is unavailable for temporary authorization key, not bound to permanent.', - 'SESSION_PASSWORD_NEEDED': - 'Two-steps verification is enabled and a password is required.', - 'USER_ALREADY_PARTICIPANT': - 'The authenticated user is already a participant of the chat.', - 'INVITE_HASH_EXPIRED': - 'The chat the user tried to join has expired and is not valid anymore.', - - # 420 FLOOD - 'FLOOD_WAIT_(\d+)': 'A wait of {} seconds is required.' - } - - def __init__(self, code, message): - self.code = code - self.code_meaning = RPCError.CodeMessages[code] - - self.message = message - self.must_resend = code == 303 # ERROR_SEE_OTHER, "The request must be repeated" - - called_super = False - for key, error_msg in RPCError.ErrorMessages.items(): - match = re.match(key, message) - if match: - error_msg = '{} ({}): {}'.format( - self.message, self.code, error_msg) - - # Get additional_data, if any - if match.groups(): - self.additional_data = int(match.group(1)) - super().__init__(self, - error_msg.format(self.additional_data)) - else: - self.additional_data = None - super().__init__(self, error_msg) - - # Add another field to easily determine whether this error - # should be handled as a password-required error - self.password_required = message == 'SESSION_PASSWORD_NEEDED' - - called_super = True - break - - if not called_super: - super().__init__( - self, 'Unknown error message with code {}: {}'.format(code, - message)) - - -class BadMessageError(Exception): - """Occurs when handling a bad_message_notification""" - ErrorMessages = { - 16: - 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' - 'synchronize it using msg_id notifications and re-send the original message ' - 'with the "correct" msg_id or wrap it in a container with a new msg_id if the ' - 'original message had waited too long on the client to be transmitted).', - 17: - 'msg_id too high (similar to the previous case, the client time has to be ' - 'synchronized, and the message re-sent with the correct msg_id).', - 18: - 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' - 'to be divisible by 4).', - 19: - 'Container msg_id is the same as msg_id of a previously received message ' - '(this must never happen).', - 20: - 'Message too old, and it cannot be verified whether the server has received a ' - 'message with this msg_id or not.', - 32: - 'msg_seqno too low (the server has already received a message with a lower ' - 'msg_id but with either a higher or an equal and odd seqno).', - 33: - 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' - 'either a lower or an equal and odd seqno).', - 34: - 'An even msg_seqno expected (irrelevant message), but odd received.', - 35: 'Odd msg_seqno expected (relevant message), but even received.', - 48: - 'Incorrect server salt (in this case, the bad_server_salt response is received with ' - 'the correct salt, and the message is to be re-sent with it).', - 64: 'Invalid container.' - } - - def __init__(self, code): - super().__init__(self, BadMessageError.ErrorMessages.get( - code, - 'Unknown error code (this should not happen): {}.'.format(code))) - - self.code = code diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py new file mode 100644 index 00000000..7e781968 --- /dev/null +++ b/telethon/errors/__init__.py @@ -0,0 +1,43 @@ +import re + +from .common import ( + ReadCancelledError, InvalidParameterError, TypeNotFoundError, + InvalidChecksumError +) + +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 * + + +def rpc_message_to_error(code, message): + errors = { + 303: rpc_303_errors, + 400: rpc_400_errors, + 401: rpc_401_errors, + 420: rpc_420_errors + }.get(code, None) + + 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) + + elif code == 403: + return ForbiddenError() + + elif code == 404: + return NotFoundError() + + elif code == 500: + return ServerError() + + return RPCError('{} (code {})'.format(message, code)) diff --git a/telethon/errors/common.py b/telethon/errors/common.py new file mode 100644 index 00000000..4ce0a365 --- /dev/null +++ b/telethon/errors/common.py @@ -0,0 +1,37 @@ +"""Errors not related to the Telegram API itself""" + + +class ReadCancelledError(Exception): + """Occurs when a read operation was cancelled""" + def __init__(self): + super().__init__(self, 'The read operation was cancelled.') + + +class InvalidParameterError(Exception): + """Occurs when an invalid parameter is given, for example, + when either A or B are required but none is given""" + + +class TypeNotFoundError(Exception): + """Occurs when a type is not found, for example, + when trying to read a TLObject with an invalid constructor code""" + + def __init__(self, invalid_constructor_id): + super().__init__( + self, 'Could not find a matching Constructor ID for the TLObject ' + 'that was supposed to be read with ID {}. Most likely, a TLObject ' + 'was trying to be read when it should not be read.' + .format(hex(invalid_constructor_id))) + + self.invalid_constructor_id = invalid_constructor_id + + +class InvalidChecksumError(Exception): + def __init__(self, checksum, valid_checksum): + super().__init__( + self, + 'Invalid checksum ({} when {} was expected). This packet should be skipped.' + .format(checksum, valid_checksum)) + + self.checksum = checksum + self.valid_checksum = valid_checksum diff --git a/telethon/errors/rpc_errors.py b/telethon/errors/rpc_errors.py new file mode 100644 index 00000000..55d03ff3 --- /dev/null +++ b/telethon/errors/rpc_errors.py @@ -0,0 +1,111 @@ +class RPCError(Exception): + code = None + message = None + + +class InvalidDCError(RPCError): + """ + The request must be repeated, but directed to a different data center. + """ + code = 303 + message = 'ERROR_SEE_OTHER' + + +class BadRequestError(RPCError): + """ + The query contains errors. In the event that a request was created + using a form and contains user generated data, the user should be + notified that the data must be corrected before the query is repeated. + """ + code = 400 + message = 'BAD_REQUEST' + + +class UnauthorizedError(RPCError): + """ + There was an unauthorized attempt to use functionality available only + to authorized users. + """ + code = 401 + message = 'UNAUTHORIZED' + + +class ForbiddenError(RPCError): + """ + Privacy violation. For example, an attempt to write a message to + someone who has blacklisted the current user. + """ + code = 403 + message = 'FORBIDDEN' + + +class NotFoundError(RPCError): + """ + An attempt to invoke a non-existent object, such as a method. + """ + code = 404 + message = 'NOT_FOUND' + + +class FloodError(RPCError): + """ + The maximum allowed number of attempts to invoke the given method + with the given input parameters has been exceeded. For example, in an + attempt to request a large number of text messages (SMS) for the same + phone number. + """ + code = 420 + message = 'FLOOD' + + +class ServerError(RPCError): + """ + An internal server error occurred while a request was being processed + for example, there was a disruption while accessing a database or file + storage. + """ + code = 500 + message = 'INTERNAL' + + +class BadMessageError(Exception): + """Occurs when handling a bad_message_notification""" + ErrorMessages = { + 16: + 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' + 'synchronize it using msg_id notifications and re-send the original message ' + 'with the "correct" msg_id or wrap it in a container with a new msg_id if the ' + 'original message had waited too long on the client to be transmitted).', + 17: + 'msg_id too high (similar to the previous case, the client time has to be ' + 'synchronized, and the message re-sent with the correct msg_id).', + 18: + 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' + 'to be divisible by 4).', + 19: + 'Container msg_id is the same as msg_id of a previously received message ' + '(this must never happen).', + 20: + 'Message too old, and it cannot be verified whether the server has received a ' + 'message with this msg_id or not.', + 32: + 'msg_seqno too low (the server has already received a message with a lower ' + 'msg_id but with either a higher or an equal and odd seqno).', + 33: + 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' + 'either a lower or an equal and odd seqno).', + 34: + 'An even msg_seqno expected (irrelevant message), but odd received.', + 35: 'Odd msg_seqno expected (relevant message), but even received.', + 48: + 'Incorrect server salt (in this case, the bad_server_salt response is received with ' + 'the correct salt, and the message is to be re-sent with it).', + 64: 'Invalid container.' + } + + def __init__(self, code): + super().__init__(self, self.ErrorMessages.get( + code, + 'Unknown error code (this should not happen): {}.'.format(code))) + + self.code = code diff --git a/telethon/errors/rpc_errors_303.py b/telethon/errors/rpc_errors_303.py new file mode 100644 index 00000000..fcef7825 --- /dev/null +++ b/telethon/errors/rpc_errors_303.py @@ -0,0 +1,51 @@ +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_303_errors = { + '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 new file mode 100644 index 00000000..57966fd6 --- /dev/null +++ b/telethon/errors/rpc_errors_400.py @@ -0,0 +1,364 @@ +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 ChannelInvalidError(BadRequestError): + def __init__(self, **kwargs): + super(Exception, self).__init__( + self, + 'Invalid channel object. Make sure to pass the right types.' + ) + + +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.' + ) + + +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 LastNameInvalidError(BadRequestError): + def __init__(self, **kwargs): + super(Exception, self).__init__( + self, + 'The last name is invalid.' + ) + + +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 MsgWaitFailedError(BadRequestError): + def __init__(self, **kwargs): + super(Exception, self).__init__( + self, + 'A waiting call returned an error.' + ) + + +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,32}"' + ) + + +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, + 'See issue #96 for Telethon - try upgrading the library.' + ) + + +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.' + ) + + +rpc_400_errors = { + 'API_ID_INVALID': ApiIdInvalidError, + 'BOT_METHOD_INVALID': BotMethodInvalidError, + 'CHANNEL_INVALID': ChannelInvalidError, + '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, + 'LASTNAME_INVALID': LastNameInvalidError, + 'MD5_CHECKSUM_INVALID': Md5ChecksumInvalidError, + 'MESSAGE_EMPTY': MessageEmptyError, + 'MESSAGE_ID_INVALID': MessageIdInvalidError, + 'MESSAGE_TOO_LONG': MessageTooLongError, + 'MSG_WAIT_FAILED': MsgWaitFailedError, + '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 new file mode 100644 index 00000000..03ebb3fb --- /dev/null +++ b/telethon/errors/rpc_errors_401.py @@ -0,0 +1,98 @@ +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_401_errors = { + '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 new file mode 100644 index 00000000..3c760bc8 --- /dev/null +++ b/telethon/errors/rpc_errors_420.py @@ -0,0 +1,16 @@ +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_420_errors = { + 'FLOOD_WAIT_(\d+)': FloodWaitError +} diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index f3a7ed01..87d536c2 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -4,8 +4,7 @@ from threading import RLock from .. import helpers as utils from ..crypto import AES -from ..errors import (BadMessageError, FloodWaitError, - RPCError, InvalidDCError) +from ..errors import BadMessageError, InvalidDCError, rpc_message_to_error from ..tl.all_tlobjects import tlobjects from ..tl.types import MsgsAck from ..extensions import BinaryReader, BinaryWriter @@ -106,7 +105,7 @@ class MtProtoSender: if updates: break # No request but one update read, exit elif request.confirm_received: - break # Request, and result read, exit + break # Request, and result read, exit self._logger.info('Request result received') self._logger.debug('receive() released the lock') @@ -325,15 +324,16 @@ class MtProtoSender: request.confirm_received = True if inner_code == 0x2144ca19: # RPC Error - error = RPCError( - code=reader.read_int(), message=reader.tgread_string()) + error = rpc_message_to_error( + reader.read_int(), reader.tgread_string()) # Acknowledge that we received the error self._need_confirmation.append(request_id) self._send_acknowledges() self._logger.warning('Read RPC error: %s', str(error)) - if error.must_resend: + if isinstance(error, InvalidDCError): + # Must resend this request if not request: raise ValueError( 'The previously sent request must be resent. ' @@ -341,15 +341,7 @@ class MtProtoSender: '(possibly called from a different thread).') request.confirm_received = False - if error.message.startswith('FLOOD_WAIT_'): - self._updates_thread_sleep = error.additional_data - raise FloodWaitError(seconds=error.additional_data) - - elif '_MIGRATE_' in error.message: - raise InvalidDCError(error) - - else: - raise error + raise error else: if not request: raise ValueError( diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 421ad923..e8760937 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -9,15 +9,16 @@ from . import TelegramBareClient # Import some externalized utilities to work with the Telegram types and more from . import helpers as utils -from .errors import (RPCError, InvalidDCError, InvalidParameterError, - ReadCancelledError) -from .network import authenticator, MtProtoSender, TcpTransport +from .errors import (RPCError, UnauthorizedError, InvalidParameterError, + ReadCancelledError, FileMigrateError, PhoneMigrateError, + NetworkMigrateError, UserMigrateError, PhoneCodeEmptyError, + PhoneCodeExpiredError, PhoneCodeHashEmptyError, + PhoneCodeInvalidError) + from .parser.markdown_parser import parse_message_entities # For sending and receiving requests from .tl import MTProtoRequest, Session, JsonSession -from .tl.all_tlobjects import layer -from .tl.functions import (InitConnectionRequest, InvokeWithLayerRequest) # Required to get the password salt from .tl.functions.account import GetPasswordRequest @@ -251,18 +252,13 @@ class TelegramClient(TelegramBareClient): # TODO Retry if 'result' is None? return result - except InvalidDCError as e: - if not e.message.startswith('FILE_MIGRATE_'): - # Only reconnect unless we're trying to download media, - # this is, on login (user migrate, phone migrate, etc.) - self._logger.info('DC error when invoking request, ' - 'attempting to reconnect at DC {}' - .format(e.new_dc)) + except (PhoneMigrateError, NetworkMigrateError, UserMigrateError) as e: + self._logger.info('DC error when invoking request, ' + 'attempting to reconnect at DC {}' + .format(e.new_dc)) - self.reconnect(new_dc=e.new_dc) - return self.invoke(request, timeout=timeout) - else: - raise + self.reconnect(new_dc=e.new_dc) + return self.invoke(request, timeout=timeout) finally: self._lock.release() @@ -326,11 +322,9 @@ class TelegramClient(TelegramBareClient): result = self.invoke(SignInRequest( phone_number, self._phone_code_hashes[phone_number], code)) - except RPCError as error: - if error.message.startswith('PHONE_CODE_'): - return None - else: - raise + except (PhoneCodeEmptyError, PhoneCodeExpiredError, + PhoneCodeHashEmptyError, PhoneCodeInvalidError): + return None elif password: salt = self.invoke(GetPasswordRequest()).current_salt @@ -386,11 +380,8 @@ class TelegramClient(TelegramBareClient): or None if the request fails (hence, not authenticated).""" try: return self.invoke(GetUsersRequest([InputUserSelf()]))[0] - except RPCError as e: - if e.code == 401: # 401 UNAUTHORIZED - return None - else: - raise + except UnauthorizedError: + return None @staticmethod def list_sessions(): @@ -743,7 +734,7 @@ class TelegramClient(TelegramBareClient): file_size=file_size, progress_callback=progress_callback ) - except InvalidDCError as e: + except FileMigrateError as e: on_dc = e.new_dc if on_dc is not None: diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index a018b54b..44637efa 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -2,7 +2,7 @@ import shutil from getpass import getpass from telethon import TelegramClient -from telethon.errors import RPCError +from telethon.errors import SessionPasswordNeededError from telethon.tl.types import UpdateShortChatMessage, UpdateShortMessage from telethon.utils import get_display_name @@ -76,14 +76,11 @@ class InteractiveTelegramClient(TelegramClient): self_user = self.sign_in(user_phone, code) # Two-step verification may be enabled - except RPCError as e: - if e.password_required: - pw = getpass('Two step verification is enabled. ' - 'Please enter your password: ') + except SessionPasswordNeededError as e: + pw = getpass('Two step verification is enabled. ' + 'Please enter your password: ') - self_user = self.sign_in(password=pw) - else: - raise + self_user = self.sign_in(password=pw) def run(self): # Listen for updates