diff --git a/.gitignore b/.gitignore index 3856ed42..a0864768 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ /telethon/_tl/fn/ /telethon/_tl/*.py /telethon/_tl/alltlobjects.py -/telethon/errors/rpcerrorlist.py +/telethon/errors/_generated.py # User session *.session diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index a2c32f70..b35e076d 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -140,6 +140,72 @@ for the library to work properly. If you still don't want it, you should subclas override the methods to do nothing. +Complete overhaul of errors +--------------------------- + +The following error name have changed to follow a better naming convention (clearer acronyms): + +* ``RPCError`` is now ``RpcError``. +* ``InvalidDCError`` is now ``InvalidDcError`` (lowercase ``c``). + +The base errors no longer have a ``.message`` field at the class-level. Instead, it is now an +attribute at the instance level (meaning you cannot do ``BadRequestError.message``, it must be +``bad_request_err.message`` where ``isinstance(bad_request_err, BadRequestError)``). + +The ``.message`` will gain its value at the time the error is constructed, rather than being +known beforehand. + +The parameter order for ``RpcError`` and all its subclasses are now ``(code, message, request)``, +as opposed to ``(message, request, code)``. + +Because Telegram errors can be added at any time, the library no longer generate a fixed set of +them. This means you can no longer use ``dir`` to get a full list of them. Instead, the errors +are automatically generated depending on the name you use for the error, with the following rules: + +* Numbers are removed from the name. The Telegram error ``FLOOD_WAIT_42`` is transformed into + ``FLOOD_WAIT_``. +* Underscores are removed from the name. ``FLOOD_WAIT_`` becomes ``FLOODWAIT``. +* Everything is lowercased. ``FLOODWAIT`` turns into ``floodwait``. +* While the name ends with ``error``, this suffix is removed. + +The only exception to this rule is ``2FA_CONFIRM_WAIT_0``, which is transformed as +``twofaconfirmwait`` (read as ``TwoFaConfirmWait``). + +What all this means is that, if Telegram raises a ``FLOOD_WAIT_42``, you can write the following: + +.. code-block:: python + + from telethon.errors import FloodWaitError + + try: + await client.send_message(chat, message) + except FloodWaitError as e: + print(f'Flood! wait for {e.seconds} seconds') + +Essentially, old code will keep working, but now you have the freedom to define even yet-to-be +discovered errors. This makes use of `PEP 562 `__ on +Python 3.7 and above and a more-hacky approach below (which your IDE may not love). + +Given the above rules, you could also write ``except errors.FLOOD_WAIT`` if you prefer to match +Telegram's naming conventions. We recommend Camel-Case naming with the "Error" suffix, but that's +up to you. + +All errors will include a list of ``.values`` (the extracted number) and ``.value`` (the first +number extracted, or ``None`` if ``values`` is empty). In addition to that, certain errors have +a more-recognizable alias (such as ``FloodWait`` which has ``.seconds`` for its ``.value``). + +The ``telethon.errors`` module continues to provide certain predefined ``RpcError`` to match on +the *code* of the error and not its message (for instance, match all errors with code 403 with +``ForbiddenError``). Note that a certain error message can appear with different codes too, this +is decided by Telegram. + +The ``telethon.errors`` module continues to provide custom errors used by the library such as +``TypeNotFoundError``. + +// TODO keep RPCError around? eh idk how much it's used +// TODO should RpcError subclass ValueError? technically the values used in the request somehow were wrong… +// TODO provide a way to see which errors are known in the docs or at tl.telethon.dev + The "iter" variant of the client methods have been removed ---------------------------------------------------------- @@ -385,6 +451,8 @@ However, you're encouraged to change uses of ``.raw_text`` with ``.message``, an either ``.md_text`` or ``.html_text`` as needed. This is because both ``.text`` and ``.raw_text`` may disappear in future versions, and their behaviour is not immediately obvious. +// TODO actually provide the things mentioned here + Using a flat list to define buttons will now create rows and not columns ------------------------------------------------------------------------ diff --git a/setup.py b/setup.py index c82d236d..05f0f7b6 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ GENERATOR_DIR = Path('telethon_generator') LIBRARY_DIR = Path('telethon') ERRORS_IN = GENERATOR_DIR / 'data/errors.csv' -ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py' +ERRORS_OUT = LIBRARY_DIR / 'errors/_generated.py' METHODS_IN = GENERATOR_DIR / 'data/methods.csv' diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index ffa06ac7..205113d6 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -676,7 +676,7 @@ async def get_permissions( for participant in chat.full_chat.participants.participants: if participant.user_id == user.user_id: return _custom.ParticipantPermissions(participant, True) - raise errors.UserNotParticipantError(None) + raise errors.USER_NOT_PARTICIPANT(400, 'USER_NOT_PARTICIPANT') raise ValueError('You must pass either a channel or a chat') @@ -694,7 +694,7 @@ async def get_stats( try: req = _tl.fn.stats.GetMessageStats(entity, message) return await self(req) - except errors.StatsMigrateError as e: + except errors.STATS_MIGRATE as e: dc = e.dc else: # Don't bother fetching the Channel entity (costs a request), instead @@ -703,13 +703,13 @@ async def get_stats( try: req = _tl.fn.stats.GetBroadcastStats(entity) return await self(req) - except errors.StatsMigrateError as e: + except errors.STATS_MIGRATE as e: dc = e.dc - except errors.BroadcastRequiredError: + except errors.BROADCAST_REQUIRED: req = _tl.fn.stats.GetMegagroupStats(entity) try: return await self(req) - except errors.StatsMigrateError as e: + except errors.STATS_MIGRATE as e: dc = e.dc sender = await self._borrow_exported_sender(dc) diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index 1277a3a4..444a0570 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -232,7 +232,7 @@ async def delete_dialog( result = await self(_tl.fn.messages.DeleteChatUser( entity.chat_id, _tl.InputUserSelf(), revoke_history=revoke )) - except errors.PeerIdInvalidError: + except errors.PEER_ID_INVALID: # Happens if we didn't have the deactivated information result = None else: diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 06f6bac8..a4349c19 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -12,8 +12,8 @@ from .. import version, helpers, __name__ as __base_name__, _tl from .._crypto import rsa from .._misc import markdown, entitycache, statecache, enums from .._network import MTProtoSender, Connection, ConnectionTcpFull, connection as conns -from ..sessions import Session, SQLiteSession, MemorySession -from ..sessions.types import DataCenter, SessionState +from .._sessions import Session, SQLiteSession, MemorySession +from .._sessions.types import DataCenter, SessionState DEFAULT_DC_ID = 2 DEFAULT_IPV4_IP = '149.154.167.51' diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 2a03a0b3..da60b24c 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -8,7 +8,8 @@ import traceback import typing import logging -from .. import events, utils, errors, _tl +from .. import events, utils, _tl +from ..errors._rpcbase import RpcError from ..events.common import EventBuilder, EventCommon if typing.TYPE_CHECKING: @@ -244,7 +245,7 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p await _get_difference(self, update, channel_id, pts_date) except OSError: pass # We were disconnected, that's okay - except errors.RPCError: + except RpcError: # There's a high chance the request fails because we lack # the channel. Because these "happen sporadically" (#1428) # we should be okay (no flood waits) even if more occur. @@ -418,7 +419,7 @@ async def _handle_auto_reconnect(self: 'TelegramClient'): await self.catch_up() self._log[__name__].info('Successfully fetched missed updates') - except errors.RPCError as e: + except RpcError as e: self._log[__name__].warning('Failed to get missed updates after ' 'reconnect: %r', e) except Exception: diff --git a/telethon/_client/users.py b/telethon/_client/users.py index b653af94..de8a49a3 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -4,10 +4,11 @@ import itertools import time import typing +from ..errors._custom import MultiError +from ..errors._rpcbase import RpcError, ServerError, FloodError, InvalidDcError, UnauthorizedError from .. import errors, hints, _tl from .._misc import helpers, utils -from ..errors import MultiError, RPCError -from ..sessions.types import Entity +from .._sessions.types import Entity _NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') @@ -49,7 +50,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl await asyncio.sleep(diff) self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) else: - raise errors.FloodWaitError(request=r, capture=diff) + raise errors.FLOOD_WAIT(420, f'FLOOD_WAIT_{diff}', request=r) if self._no_updates: r = _tl.fn.InvokeWithoutUpdates(r) @@ -67,7 +68,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl for f in future: try: result = await f - except RPCError as e: + except RpcError as e: exceptions.append(e) results.append(None) continue @@ -87,22 +88,20 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl if entities: await self.session.insert_entities(entities) return result - except (errors.ServerError, errors.RpcCallFailError, - errors.RpcMcgetFailError, errors.InterdcCallErrorError, - errors.InterdcCallRichErrorError) as e: + except ServerError as e: last_error = e self._log[__name__].warning( 'Telegram is having internal issues %s: %s', e.__class__.__name__, e) await asyncio.sleep(2) - except (errors.FloodWaitError, errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e: + except FloodError as e: last_error = e if utils.is_list_like(request): request = request[request_index] - # SLOW_MODE_WAIT is chat-specific, not request-specific - if not isinstance(e, errors.SlowModeWaitError): + # SLOWMODE_WAIT is chat-specific, not request-specific + if not isinstance(e, errors.SLOWMODE_WAIT): self._flood_waited_requests\ [request.CONSTRUCTOR_ID] = time.time() + e.seconds @@ -116,12 +115,11 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl await asyncio.sleep(e.seconds) else: raise - except (errors.PhoneMigrateError, errors.NetworkMigrateError, - errors.UserMigrateError) as e: + except InvalidDcError as e: last_error = e self._log[__name__].info('Phone migrated to %d', e.new_dc) should_raise = isinstance(e, ( - errors.PhoneMigrateError, errors.NetworkMigrateError + errors.PHONE_MIGRATE, errors.NETWORK_MIGRATE )) if should_raise and await self.is_user_authorized(): raise @@ -138,7 +136,7 @@ async def get_me(self: 'TelegramClient', input_peer: bool = False) \ try: me = (await self(_tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0] return utils.get_input_peer(me, allow_self=False) if input_peer else me - except errors.UnauthorizedError: + except UnauthorizedError: return None async def is_bot(self: 'TelegramClient') -> bool: @@ -150,7 +148,7 @@ async def is_user_authorized(self: 'TelegramClient') -> bool: # Any request that requires authorization will work await self(_tl.fn.updates.GetState()) self._authorized = True - except errors.RPCError: + except RpcError: self._authorized = False return self._authorized @@ -290,7 +288,7 @@ async def get_input_entity( channels = await self(_tl.fn.channels.GetChannels([ _tl.InputChannel(peer.channel_id, access_hash=0)])) return utils.get_input_peer(channels.chats[0]) - except errors.ChannelInvalidError: + except errors.CHANNEL_INVALID: pass raise ValueError( @@ -338,7 +336,7 @@ async def _get_entity_from_string(self: 'TelegramClient', string): _tl.fn.contacts.GetContacts(0))).users: if user.phone == phone: return user - except errors.BotMethodInvalidError: + except errors.BOT_METHOD_INVALID: raise ValueError('Cannot get entity by phone number as a ' 'bot (try using integer IDs, not strings)') elif string.lower() in ('me', 'self'): @@ -360,7 +358,7 @@ async def _get_entity_from_string(self: 'TelegramClient', string): try: result = await self( _tl.fn.contacts.ResolveUsername(username)) - except errors.UsernameNotOccupiedError as e: + except errors.USERNAME_NOT_OCCUPIED as e: raise ValueError('No user has "{}" as username' .format(username)) from e diff --git a/telethon/_misc/binaryreader.py b/telethon/_misc/binaryreader.py index b0be805b..4117653f 100644 --- a/telethon/_misc/binaryreader.py +++ b/telethon/_misc/binaryreader.py @@ -7,7 +7,7 @@ from datetime import datetime, timezone, timedelta from io import BytesIO from struct import unpack -from ..errors import TypeNotFoundError +from ..errors._custom import TypeNotFoundError from .. import _tl from ..types import _core diff --git a/telethon/_misc/entitycache.py b/telethon/_misc/entitycache.py index 685aa411..a5be14c9 100644 --- a/telethon/_misc/entitycache.py +++ b/telethon/_misc/entitycache.py @@ -3,7 +3,7 @@ import itertools from .._misc import utils from .. import _tl -from ..sessions.types import Entity +from .._sessions.types import Entity # Which updates have the following fields? _has_field = { diff --git a/telethon/_network/authenticator.py b/telethon/_network/authenticator.py index d5b18c56..533eeb5c 100644 --- a/telethon/_network/authenticator.py +++ b/telethon/_network/authenticator.py @@ -8,7 +8,7 @@ from hashlib import sha1 from .. import helpers, _tl from .._crypto import AES, AuthKey, Factorization, rsa -from ..errors import SecurityError +from ..errors._custom import SecurityError from .._misc.binaryreader import BinaryReader diff --git a/telethon/_network/connection/connection.py b/telethon/_network/connection/connection.py index 8bff043d..d206b185 100644 --- a/telethon/_network/connection/connection.py +++ b/telethon/_network/connection/connection.py @@ -13,7 +13,7 @@ try: except ImportError: python_socks = None -from ...errors import InvalidChecksumError +from ...errors._custom import InvalidChecksumError from ... import helpers diff --git a/telethon/_network/connection/tcpfull.py b/telethon/_network/connection/tcpfull.py index 7ebbbe6f..cd60e693 100644 --- a/telethon/_network/connection/tcpfull.py +++ b/telethon/_network/connection/tcpfull.py @@ -2,7 +2,7 @@ import struct from zlib import crc32 from .connection import Connection, PacketCodec -from ...errors import InvalidChecksumError +from ...errors._custom import InvalidChecksumError class FullPacketCodec(PacketCodec): diff --git a/telethon/_network/mtprotoplainsender.py b/telethon/_network/mtprotoplainsender.py index 3a5cffed..427f27ce 100644 --- a/telethon/_network/mtprotoplainsender.py +++ b/telethon/_network/mtprotoplainsender.py @@ -5,7 +5,7 @@ in plain text, when no authorization key has been created yet. import struct from .mtprotostate import MTProtoState -from ..errors import InvalidBufferError +from ..errors._custom import InvalidBufferError from .._misc.binaryreader import BinaryReader diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index 16bd6e32..20e68d72 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -4,6 +4,7 @@ import struct from . import authenticator from .._misc.messagepacker import MessagePacker +from ..errors._rpcbase import _mk_error_type from .mtprotoplainsender import MTProtoPlainSender from .requeststate import RequestState from .mtprotostate import MTProtoState @@ -585,12 +586,19 @@ class MTProtoSender: return if rpc_result.error: - error = rpc_message_to_error(rpc_result.error, state.request) self._send_queue.append( RequestState(_tl.MsgsAck([state.msg_id]))) if not state.future.cancelled(): - state.future.set_exception(error) + err_ty = _mk_error_type( + name=rpc_result.error.error_message, + code=rpc_result.error.error_code, + ) + state.future.set_exception(err_ty( + rpc_result.error.error_code, + rpc_result.error.error_message, + state.request + )) else: try: with BinaryReader(rpc_result.body) as reader: diff --git a/telethon/_network/mtprotostate.py b/telethon/_network/mtprotostate.py index f96554ac..19578da3 100644 --- a/telethon/_network/mtprotostate.py +++ b/telethon/_network/mtprotostate.py @@ -4,7 +4,7 @@ import time from hashlib import sha256 from .._crypto import AES -from ..errors import SecurityError, InvalidBufferError +from ..errors._custom import SecurityError, InvalidBufferError from .._misc.binaryreader import BinaryReader from ..types._core import TLMessage, GzipPacked from .._misc.tlobject import TLRequest diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index a50ae36b..152e7823 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -1,46 +1,48 @@ -""" -This module holds all the base and automatically generated errors that the -Telegram API has. See telethon_generator/errors.json for more. -""" -import re +import sys -from .common import ( - ReadCancelledError, TypeNotFoundError, InvalidChecksumError, - InvalidBufferError, SecurityError, CdnFileTamperedError, - BadMessageError, MultiError +from ._custom import ( + ReadCancelledError, + TypeNotFoundError, + InvalidChecksumError, + InvalidBufferError, + SecurityError, + CdnFileTamperedError, + BadMessageError, + MultiError, +) +from ._rpcbase import ( + RpcError, + InvalidDcError, + BadRequestError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + AuthKeyError, + FloodError, + ServerError, + BotTimeout, + TimedOutError, + _mk_error_type ) -# This imports the base errors too, as they're imported there -from .rpcbaseerrors import * -from .rpcerrorlist import * +if sys.version_info < (3, 7): + # https://stackoverflow.com/a/7668273/ + class _TelethonErrors: + def __init__(self, _mk_error_type, everything): + self._mk_error_type = _mk_error_type + self.__dict__.update({ + k: v + for k, v in everything.items() + if isinstance(v, type) and issubclass(v, Exception) + }) + def __getattr__(self, name): + return self._mk_error_type(name=name) -def rpc_message_to_error(rpc_error, request): - """ - Converts a Telegram's RPC Error to a Python error. + sys.modules[__name__] = _TelethonErrors(_mk_error_type, globals()) +else: + # https://www.python.org/dev/peps/pep-0562/ + def __getattr__(name): + return _mk_error_type(name=name) - :param rpc_error: the RpcError instance. - :param request: the request that caused this error. - :return: the RPCError as a Python exception that represents this error. - """ - # Try to get the error by direct look-up, otherwise regex - # Case-insensitive, for things like "timeout" which don't conform. - cls = rpc_errors_dict.get(rpc_error.error_message.upper(), None) - if cls: - return cls(request=request) - - for msg_regex, cls in rpc_errors_re: - m = re.match(msg_regex, rpc_error.error_message) - if m: - capture = int(m.group(1)) if m.groups() else None - return cls(request=request, capture=capture) - - # Some errors are negative: - # * -500 for "No workers running", - # * -503 for "Timeout" - # - # We treat them as if they were positive, so -500 will be treated - # as a `ServerError`, etc. - cls = base_errors.get(abs(rpc_error.error_code), RPCError) - return cls(request=request, message=rpc_error.error_message, - code=rpc_error.error_code) +del sys diff --git a/telethon/errors/common.py b/telethon/errors/_custom.py similarity index 100% rename from telethon/errors/common.py rename to telethon/errors/_custom.py diff --git a/telethon/errors/_rpcbase.py b/telethon/errors/_rpcbase.py new file mode 100644 index 00000000..d074be26 --- /dev/null +++ b/telethon/errors/_rpcbase.py @@ -0,0 +1,144 @@ +import re + +from ._generated import _captures, _descriptions +from .. import _tl + + +_NESTS_QUERY = ( + _tl.fn.InvokeAfterMsg, + _tl.fn.InvokeAfterMsgs, + _tl.fn.InitConnection, + _tl.fn.InvokeWithLayer, + _tl.fn.InvokeWithoutUpdates, + _tl.fn.InvokeWithMessagesRange, + _tl.fn.InvokeWithTakeout, +) + + +class RpcError(Exception): + def __init__(self, code, message, request=None): + doc = self.__doc__ + if doc is None: + doc = ( + '\n Please report this error at https://github.com/LonamiWebs/Telethon/issues/3169' + '\n (the library is not aware of it yet and we would appreciate your help, thank you!)' + ) + elif not doc: + doc = '(no description available)' + + super().__init__(f'{message}, code={code}{self._fmt_request(request)}: {doc}') + self.code = code + self.message = message + self.request = request + # Special-case '2fa' to exclude the 2 from values + self.values = [int(x) for x in re.findall(r'-?\d+', re.sub(r'^2fa', '', self.message, flags=re.IGNORECASE))] + self.value = self.values[0] if self.values else None + + @staticmethod + def _fmt_request(request): + if not request: + return '' + + n = 0 + reason = '' + while isinstance(request, _NESTS_QUERY): + n += 1 + reason += request.__class__.__name__ + '(' + request = request.query + reason += request.__class__.__name__ + ')' * n + + return ', request={}'.format(reason) + + def __reduce__(self): + return type(self), (self.request, self.message, self.code) + + +def _mk_error_type(*, name=None, code=None, doc=None, _errors={}) -> type: + if name is None and code is None: + raise ValueError('at least one of `name` or `code` must be provided') + + if name is not None: + # Special-case '2fa' to 'twofa' + name = re.sub(r'^2fa', 'twofa', name, flags=re.IGNORECASE) + + # Get canonical name + name = re.sub(r'[-_\d]', '', name).lower() + while name.endswith('error'): + name = name[:-len('error')] + + doc = _descriptions.get(name, doc) + capture_alias = _captures.get(name) + + d = {'__doc__': doc} + + if capture_alias: + d[capture_alias] = property( + fget=lambda s: s.value, + doc='Alias for `self.value`. Useful to make the code easier to follow.' + ) + + if (name, None) not in _errors: + _errors[(name, None)] = type(f'RpcError{name.title()}', (RpcError,), d) + + if code is not None: + # Pretend negative error codes are positive + code = str(abs(code)) + if (None, code) not in _errors: + _errors[(None, code)] = type(f'RpcError{code}', (RpcError,), {'__doc__': doc}) + + if (name, code) not in _errors: + specific = _errors[(name, None)] + base = _errors[(None, code)] + _errors[(name, code)] = type(f'RpcError{name.title()}{code}', (specific, base), {'__doc__': doc}) + + return _errors[(name, code)] + + +InvalidDcError = _mk_error_type(code=303, doc=""" + The request must be repeated, but directed to a different data center. +""") + +BadRequestError = _mk_error_type(code=400, doc=""" + 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. +""") + +UnauthorizedError = _mk_error_type(code=401, doc=""" + There was an unauthorized attempt to use functionality available only + to authorized users. +""") + +ForbiddenError = _mk_error_type(code=403, doc=""" + Privacy violation. For example, an attempt to write a message to + someone who has blacklisted the current user. +""") + +NotFoundError = _mk_error_type(code=404, doc=""" + An attempt to invoke a non-existent object, such as a method. +""") + +AuthKeyError = _mk_error_type(code=406, doc=""" + Errors related to invalid authorization key, like + AUTH_KEY_DUPLICATED which can cause the connection to fail. +""") + +FloodError = _mk_error_type(code=420, doc=""" + 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. +""") + +# Witnessed as -500 for "No workers running" +ServerError = _mk_error_type(code=500, doc=""" + An internal server error occurred while a request was being processed + for example, there was a disruption while accessing a database or file + storage. +""") + +# Witnessed as -503 for "Timeout" +BotTimeout = TimedOutError = _mk_error_type(code=503, doc=""" + Clicking the inline buttons of bots that never (or take to long to) + call ``answerCallbackQuery`` will result in this "special" RPCError. +""") diff --git a/telethon/errors/rpcbaseerrors.py b/telethon/errors/rpcbaseerrors.py deleted file mode 100644 index d90bbb02..00000000 --- a/telethon/errors/rpcbaseerrors.py +++ /dev/null @@ -1,131 +0,0 @@ -from .. import _tl - -_NESTS_QUERY = ( - _tl.fn.InvokeAfterMsg, - _tl.fn.InvokeAfterMsgs, - _tl.fn.InitConnection, - _tl.fn.InvokeWithLayer, - _tl.fn.InvokeWithoutUpdates, - _tl.fn.InvokeWithMessagesRange, - _tl.fn.InvokeWithTakeout, -) - -class RPCError(Exception): - """Base class for all Remote Procedure Call errors.""" - code = None - message = None - - def __init__(self, request, message, code=None): - super().__init__('RPCError {}: {}{}'.format( - code or self.code, message, self._fmt_request(request))) - - self.request = request - self.code = code - self.message = message - - @staticmethod - def _fmt_request(request): - n = 0 - reason = '' - while isinstance(request, _NESTS_QUERY): - n += 1 - reason += request.__class__.__name__ + '(' - request = request.query - reason += request.__class__.__name__ + ')' * n - - return ' (caused by {})'.format(reason) - - def __reduce__(self): - return type(self), (self.request, self.message, self.code) - - -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 AuthKeyError(RPCError): - """ - Errors related to invalid authorization key, like - AUTH_KEY_DUPLICATED which can cause the connection to fail. - """ - code = 406 - message = 'AUTH_KEY' - - -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 # Also witnessed as -500 - message = 'INTERNAL' - - -class TimedOutError(RPCError): - """ - Clicking the inline buttons of bots that never (or take to long to) - call ``answerCallbackQuery`` will result in this "special" RPCError. - """ - code = 503 # Only witnessed as -503 - message = 'Timeout' - - -BotTimeout = TimedOutError - - -base_errors = {x.code: x for x in ( - InvalidDCError, BadRequestError, UnauthorizedError, ForbiddenError, - NotFoundError, AuthKeyError, FloodError, ServerError, TimedOutError -)} diff --git a/telethon/types/_custom/draft.py b/telethon/types/_custom/draft.py index e81f85ad..82e0cb26 100644 --- a/telethon/types/_custom/draft.py +++ b/telethon/types/_custom/draft.py @@ -1,7 +1,7 @@ import datetime from ... import _tl -from ...errors import RPCError +from ...errors._rpcbase import RpcError from ..._misc import markdown, tlobject from ..._misc.utils import get_input_peer, get_peer @@ -169,7 +169,7 @@ class Draft: def to_dict(self): try: entity = self.entity - except RPCError as e: + except RpcError as e: entity = e return { diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index ae0b395e..4d5d615a 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -6,7 +6,7 @@ from .messagebutton import MessageButton from .forward import Forward from .file import File from ..._misc import utils, tlobject -from ... import errors, _tl +from ... import _tl def _fwd(field, doc): diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index ff9d8168..71a417d8 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -1,5 +1,5 @@ name,codes,description -2FA_CONFIRM_WAIT_X,420,The account is 2FA protected so it will be deleted in a week. Otherwise it can be reset in {seconds} +2FA_CONFIRM_WAIT_0,420,The account is 2FA protected so it will be deleted in a week. Otherwise it can be reset in {seconds} ABOUT_TOO_LONG,400,The provided bio is too long ACCESS_TOKEN_EXPIRED,400,Bot token expired ACCESS_TOKEN_INVALID,400,The provided token is not valid @@ -101,7 +101,7 @@ DH_G_A_INVALID,400,g_a invalid DOCUMENT_INVALID,400,The document file was invalid and can't be used in inline mode EMAIL_HASH_EXPIRED,400,The email hash expired and cannot be used to verify it EMAIL_INVALID,400,The given email is invalid -EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}" +EMAIL_UNCONFIRMED_0,400,"Email unconfirmed, the length of the code must be {code_length}" EMOJI_INVALID,400, EMOJI_NOT_MODIFIED,400, EMOTICON_EMPTY,400,The emoticon field cannot be empty @@ -124,22 +124,21 @@ FIELD_NAME_INVALID,400,The field with the name FIELD_NAME is invalid FILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again FILE_CONTENT_TYPE_INVALID,400, FILE_ID_INVALID,400,"The provided file id is invalid. Make sure all parameters are present, have the correct type and are not empty (ID, access hash, file reference, thumb size ...)" -FILE_MIGRATE_X,303,The file to be accessed is currently stored in DC {new_dc} +FILE_MIGRATE_0,303,The file to be accessed is currently stored in DC {new_dc} FILE_PARTS_INVALID,400,The number of file parts is invalid -FILE_PART_0_MISSING,400,File part 0 missing FILE_PART_EMPTY,400,The provided file part is empty FILE_PART_INVALID,400,The file part number is invalid FILE_PART_LENGTH_INVALID,400,The length of a file part is invalid FILE_PART_SIZE_CHANGED,400,The file part size (chunk size) cannot change during upload FILE_PART_SIZE_INVALID,400,The provided file part size is invalid -FILE_PART_X_MISSING,400,Part {which} of the file is missing from storage +FILE_PART_0_MISSING,400,Part {which} of the file is missing from storage FILE_REFERENCE_EMPTY,400,The file reference must exist to access the media and it cannot be empty FILE_REFERENCE_EXPIRED,400,The file reference has expired and is no longer valid or it belongs to self-destructing media and cannot be resent FILE_REFERENCE_INVALID,400,The file reference is invalid or you can't do that operation on such message FILE_TITLE_EMPTY,400, FIRSTNAME_INVALID,400,The first name is invalid -FLOOD_TEST_PHONE_WAIT_X,420,A wait of {seconds} seconds is required in the test servers -FLOOD_WAIT_X,420,A wait of {seconds} seconds is required +FLOOD_TEST_PHONE_WAIT_0,420,A wait of {seconds} seconds is required in the test servers +FLOOD_WAIT_0,420,A wait of {seconds} seconds is required FOLDER_ID_EMPTY,400,The folder you tried to delete was already empty FOLDER_ID_INVALID,400,The folder you tried to use was not valid FRESH_CHANGE_ADMINS_FORBIDDEN,400,Recently logged-in users cannot add or change admins @@ -175,8 +174,8 @@ INPUT_LAYER_INVALID,400,The provided layer is invalid INPUT_METHOD_INVALID,400,The invoked method does not exist anymore or has never existed INPUT_REQUEST_TOO_LONG,400,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) INPUT_USER_DEACTIVATED,400,The specified user was deleted -INTERDC_X_CALL_ERROR,500,An error occurred while communicating with DC {dc} -INTERDC_X_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc} +INTERDC_0_CALL_ERROR,500,An error occurred while communicating with DC {dc} +INTERDC_0_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc} INVITE_HASH_EMPTY,400,The invite hash is empty INVITE_HASH_EXPIRED,400,The chat the user tried to join has expired and is not valid anymore INVITE_HASH_INVALID,400,The invite hash is invalid @@ -218,7 +217,7 @@ MT_SEND_QUEUE_TOO_LONG,500, MULTI_MEDIA_TOO_LONG,400,Too many media files were included in the same album NEED_CHAT_INVALID,500,The provided chat is invalid NEED_MEMBER_INVALID,500,The provided member is invalid or does not exist (for example a thumb size) -NETWORK_MIGRATE_X,303,The source IP address is associated with DC {new_dc} +NETWORK_MIGRATE_0,303,The source IP address is associated with DC {new_dc} NEW_SALT_INVALID,400,The new salt is invalid NEW_SETTINGS_INVALID,400,The new settings are invalid NEXT_OFFSET_INVALID,400,The value for next_offset is invalid. Check that it has normal characters and is not too long @@ -238,7 +237,7 @@ PASSWORD_MISSING,400,The account must have 2-factor authentication enabled (a pa PASSWORD_RECOVERY_EXPIRED,400, PASSWORD_RECOVERY_NA,400, PASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used -PASSWORD_TOO_FRESH_X,400,The password was added too recently and {seconds} seconds must pass before using the method +PASSWORD_TOO_FRESH_0,400,The password was added too recently and {seconds} seconds must pass before using the method PAYMENT_PROVIDER_INVALID,400,The payment provider was not recognised or its token was invalid PEER_FLOOD,400,Too many requests PEER_ID_INVALID,400,"An invalid Peer was used. Make sure to pass the right peer type and that the value is valid (for instance, bots cannot start conversations)" @@ -250,7 +249,7 @@ PHONE_CODE_EMPTY,400,The phone code is missing PHONE_CODE_EXPIRED,400,The confirmation code has expired PHONE_CODE_HASH_EMPTY,400,The phone code hash is missing PHONE_CODE_INVALID,400,The phone code entered was invalid -PHONE_MIGRATE_X,303,The phone number a user is trying to use for authorization is associated with DC {new_dc} +PHONE_MIGRATE_0,303,The phone number a user is trying to use for authorization is associated with DC {new_dc} PHONE_NUMBER_APP_SIGNUP_FORBIDDEN,400,You can't sign up using this app PHONE_NUMBER_BANNED,400,The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam PHONE_NUMBER_FLOOD,400,You asked for the code too many times. @@ -276,7 +275,7 @@ POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too lon POLL_QUESTION_INVALID,400,The poll question was either empty or too long POLL_UNSUPPORTED,400,This layer does not support polls in the issued method POLL_VOTE_REQUIRED,403, -PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN,406,"Similar to a flood wait, must wait {minutes} minutes" +PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_0MIN,406,"Similar to a flood wait, must wait {minutes} minutes" PRIVACY_KEY_INVALID,400,The privacy key is invalid PRIVACY_TOO_LONG,400,Cannot add that many entities in a single request PRIVACY_VALUE_INVALID,400,The privacy value is invalid @@ -322,16 +321,16 @@ SENSITIVE_CHANGE_FORBIDDEN,403,Your sensitive content settings cannot be changed SESSION_EXPIRED,401,The authorization has expired SESSION_PASSWORD_NEEDED,401,Two-steps verification is enabled and a password is required SESSION_REVOKED,401,"The authorization has been invalidated, because of the user terminating all sessions" -SESSION_TOO_FRESH_X,400,The session logged in too recently and {seconds} seconds must pass before calling the method +SESSION_TOO_FRESH_0,400,The session logged in too recently and {seconds} seconds must pass before calling the method SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name SHORT_NAME_INVALID,400, SHORT_NAME_OCCUPIED,400, -SLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat +SLOWMODE_WAIT_0,420,A wait of {seconds} seconds is required before sending another message in this chat SRP_ID_INVALID,400, START_PARAM_EMPTY,400,The start parameter is empty START_PARAM_INVALID,400,Start parameter invalid -STATS_MIGRATE_X,303,The channel statistics must be fetched from DC {dc} +STATS_MIGRATE_0,303,The channel statistics must be fetched from DC {dc} STICKERSET_INVALID,400,The provided sticker set is invalid STICKERSET_OWNER_ANONYMOUS,406,This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins STICKERS_EMPTY,400,No sticker provided @@ -348,7 +347,7 @@ STICKER_THUMB_PNG_NOPNG,400,Stickerset thumb must be a png file but the used fil STICKER_THUMB_TGS_NOTGS,400,Stickerset thumb must be a tgs file but the used file was not tgs STORAGE_CHECK_FAILED,500,Server storage check failed STORE_INVALID_SCALAR_TYPE,500, -TAKEOUT_INIT_DELAY_X,420,A wait of {seconds} seconds is required before being able to initiate the takeout +TAKEOUT_INIT_DELAY_0,420,A wait of {seconds} seconds is required before being able to initiate the takeout TAKEOUT_INVALID,400,The takeout session has been invalidated by another data export session TAKEOUT_REQUIRED,400,You must initialize a takeout request first TEMP_AUTH_KEY_EMPTY,400,No temporary auth key provided @@ -391,7 +390,7 @@ USER_INVALID,400,The given user was invalid USER_IS_BLOCKED,400 403,User is blocked USER_IS_BOT,400,Bots can't send messages to other bots USER_KICKED,400,This user was kicked from this supergroup/channel -USER_MIGRATE_X,303,The user whose identity is being used to execute queries is associated with DC {new_dc} +USER_MIGRATE_0,303,The user whose identity is being used to execute queries is associated with DC {new_dc} USER_NOT_MUTUAL_CONTACT,400 403,The provided user is not a mutual contact USER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagroup or channel USER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 4427492b..135d5618 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -7,7 +7,7 @@ account.confirmPasswordEmail,user, account.confirmPhone,user,CODE_HASH_INVALID PHONE_CODE_EMPTY account.createTheme,user,THEME_MIME_INVALID account.declinePasswordReset,user,RESET_REQUEST_MISSING -account.deleteAccount,user,2FA_CONFIRM_WAIT_X +account.deleteAccount,user,2FA_CONFIRM_WAIT_0 account.deleteSecureValue,user, account.finishTakeoutSession,user, account.getAccountTTL,user, @@ -57,7 +57,7 @@ account.setPrivacy,user,PRIVACY_KEY_INVALID PRIVACY_TOO_LONG account.unregisterDevice,user,TOKEN_INVALID account.updateDeviceLocked,user, account.updateNotifySettings,user,PEER_ID_INVALID -account.updatePasswordSettings,user,EMAIL_UNCONFIRMED_X NEW_SALT_INVALID NEW_SETTINGS_INVALID PASSWORD_HASH_INVALID +account.updatePasswordSettings,user,EMAIL_UNCONFIRMED_0 NEW_SALT_INVALID NEW_SETTINGS_INVALID PASSWORD_HASH_INVALID account.updateProfile,user,ABOUT_TOO_LONG FIRSTNAME_INVALID account.updateStatus,user,SESSION_PASSWORD_NEEDED account.updateTheme,user,THEME_INVALID @@ -97,7 +97,7 @@ channels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORB channels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED channels.editAdmin,both,ADMINS_TOO_MUCH ADMIN_RANK_EMOJI_NOT_ALLOWED ADMIN_RANK_INVALID BOT_CHANNELS_NA CHANNEL_INVALID CHAT_ADMIN_INVITE_REQUIRED CHAT_ADMIN_REQUIRED FRESH_CHANGE_ADMINS_FORBIDDEN RIGHT_FORBIDDEN USER_CREATOR USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED channels.editBanned,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ADMIN_INVALID USER_ID_INVALID -channels.editCreator,user,PASSWORD_MISSING PASSWORD_TOO_FRESH_X SESSION_TOO_FRESH_X SRP_ID_INVALID +channels.editCreator,user,PASSWORD_MISSING PASSWORD_TOO_FRESH_0 SESSION_TOO_FRESH_0 SRP_ID_INVALID channels.editLocation,user, channels.editPhoto,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED FILE_REFERENCE_INVALID PHOTO_INVALID channels.editTitle,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED CHAT_NOT_MODIFIED @@ -251,7 +251,7 @@ messages.getWebPage,user,WC_CONVERT_URL_INVALID messages.getWebPagePreview,user, messages.hidePeerSettingsBar,user, messages.importChatInvite,user,CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT -messages.initHistoryImport,user,IMPORT_FILE_INVALID IMPORT_FORMAT_UNRECOGNIZED PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN TIMEOUT +messages.initHistoryImport,user,IMPORT_FILE_INVALID IMPORT_FORMAT_UNRECOGNIZED PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_0MIN TIMEOUT messages.installStickerSet,user,STICKERSET_INVALID messages.markDialogUnread,user, messages.migrateChat,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID PEER_ID_INVALID @@ -337,8 +337,8 @@ reqPq,both, reqPqMulti,both, rpcDropAnswer,both, setClientDHParams,both, -stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_X -stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_X +stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_0 +stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_0 stats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID diff --git a/telethon_generator/generators/errors.py b/telethon_generator/generators/errors.py index 386575be..5771369c 100644 --- a/telethon_generator/generators/errors.py +++ b/telethon_generator/generators/errors.py @@ -1,60 +1,12 @@ def generate_errors(errors, f): - # Exact/regex match to create {CODE: ErrorClassName} - exact_match = [] - regex_match = [] - - # Find out what subclasses to import and which to create - import_base, create_base = set(), {} + f.write('_captures = {\n') for error in errors: - if error.subclass_exists: - import_base.add(error.subclass) - else: - create_base[error.subclass] = error.int_code + if error.capture_name: + f.write(f" {error.canonical_name!r}: {error.capture_name!r},\n") + f.write('}\n') - if error.has_captures: - regex_match.append(error) - else: - exact_match.append(error) - - # Imports and new subclass creation - f.write('from .rpcbaseerrors import RPCError, {}\n' - .format(", ".join(sorted(import_base)))) - - for cls, int_code in sorted(create_base.items(), key=lambda t: t[1]): - f.write('\n\nclass {}(RPCError):\n code = {}\n' - .format(cls, int_code)) - - # Error classes generation + f.write('\n\n_descriptions = {\n') for error in errors: - f.write('\n\nclass {}({}):\n '.format(error.name, error.subclass)) - - if error.has_captures: - f.write('def __init__(self, request, capture=0):\n ' - ' self.request = request\n ') - f.write(' self.{} = int(capture)\n ' - .format(error.capture_name)) - else: - f.write('def __init__(self, request):\n ' - ' self.request = request\n ') - - f.write('super(Exception, self).__init__(' - '{}'.format(repr(error.description))) - - if error.has_captures: - f.write('.format({0}=self.{0})'.format(error.capture_name)) - - f.write(' + self._fmt_request(self.request))\n\n') - f.write(' def __reduce__(self):\n ') - if error.has_captures: - f.write('return type(self), (self.request, self.{})\n'.format(error.capture_name)) - else: - f.write('return type(self), (self.request,)\n') - - # Create the actual {CODE: ErrorClassName} dict once classes are defined - f.write('\n\nrpc_errors_dict = {\n') - for error in exact_match: - f.write(' {}: {},\n'.format(repr(error.pattern), error.name)) - f.write('}\n\nrpc_errors_re = (\n') - for error in regex_match: - f.write(' ({}, {}),\n'.format(repr(error.pattern), error.name)) - f.write(')\n') + if error.description: + f.write(f" {error.canonical_name!r}: {error.description!r},\n") + f.write('}\n') diff --git a/telethon_generator/parsers/errors.py b/telethon_generator/parsers/errors.py index 04cd3412..0982edea 100644 --- a/telethon_generator/parsers/errors.py +++ b/telethon_generator/parsers/errors.py @@ -17,25 +17,16 @@ KNOWN_BASE_CLASSES = { } -def _get_class_name(error_code): +def _get_canonical_name(error_code): """ - Gets the corresponding class name for the given error code, - this either being an integer (thus base error name) or str. + Gets the corresponding canonical name for the given error code. """ - if isinstance(error_code, int): - return KNOWN_BASE_CLASSES.get( - abs(error_code), 'RPCError' + str(error_code).replace('-', 'Neg') - ) + # This code should match that of the library itself. + name = re.sub(r'[-_\d]', '', error_code).lower() + while name.endswith('error'): + name = name[:-len('error')] - if error_code.startswith('2'): - error_code = re.sub(r'2', 'TWO_', error_code, count=1) - - if re.match(r'\d+', error_code): - raise RuntimeError('error code starting with a digit cannot have valid Python name: {}'.format(error_code)) - - return snake_to_camel_case( - error_code.replace('FIRSTNAME', 'FIRST_NAME')\ - .replace('SLOWMODE', 'SLOW_MODE').lower(), suffix='Error') + return name class Error: @@ -45,18 +36,13 @@ class Error: # Telegram isn't exactly consistent with returned errors anyway. self.int_code = codes[0] self.str_code = name - self.subclass = _get_class_name(codes[0]) - self.subclass_exists = abs(codes[0]) in KNOWN_BASE_CLASSES + self.canonical_name = _get_canonical_name(name) self.description = description - self.has_captures = '_X' in name - if self.has_captures: - self.name = _get_class_name(name.replace('_X', '_')) - self.pattern = name.replace('_X', r'_(\d+)') + has_captures = '0' in name + if has_captures: self.capture_name = re.search(r'{(\w+)}', description).group(1) else: - self.name = _get_class_name(name) - self.pattern = name self.capture_name = None