From 91a5d20e93e54a5b2672acb6322a943863e20f5b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 7 Oct 2017 13:26:09 +0200 Subject: [PATCH 01/58] Replace .on_response with static .from_reader for all types --- telethon/extensions/binary_reader.py | 6 +-- telethon/tl/tlobject.py | 5 ++- telethon_generator/tl_generator.py | 60 +++++++++++++++------------- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 43232b0b..747d18c8 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -131,11 +131,7 @@ class BinaryReader: # If there was still no luck, give up raise TypeNotFoundError(constructor_id) - # Create an empty instance of the class and - # fill it with the read attributes - result = clazz.empty() - result.on_response(self) - return result + return clazz.from_reader(self) def tgread_vector(self): """Reads a vector (a list) of Telegram objects""" diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 1866ba68..f34ed558 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -121,5 +121,6 @@ class TLObject: def to_bytes(self): return b'' - def on_response(self, reader): - pass + @staticmethod + def from_reader(reader): + return TLObject() diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index e76dffaa..0fb2c1ed 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -335,32 +335,28 @@ class TLGenerator: builder.writeln('))') builder.end_block() - # Write the empty() function, which returns an "empty" - # instance, in which all attributes are set to None + # Write the static from_reader(reader) function builder.writeln('@staticmethod') - builder.writeln('def empty():') + builder.writeln('def from_reader(reader):') + for arg in tlobject.args: + TLGenerator.write_read_code( + builder, arg, tlobject.args, name='_' + arg.name + ) + builder.writeln('return {}({})'.format( - tlobject.class_name(), ', '.join('None' for _ in range(len(args))) + tlobject.class_name(), ', '.join( + '{0}=_{0}'.format(a.name) for a in tlobject.sorted_args() + if not a.flag_indicator and not a.generic_definition + ) )) builder.end_block() - # Write the on_response(self, reader) function - builder.writeln('def on_response(self, reader):') - # Do not read constructor's ID, since - # that's already been read somewhere else + # Only requests can have a different response that's not their + # serialized body, that is, we'll be setting their .result. if tlobject.is_function: + builder.writeln('def on_response(self, reader):') TLGenerator.write_request_result_code(builder, tlobject) - else: - if tlobject.args: - for arg in tlobject.args: - TLGenerator.write_onresponse_code( - builder, arg, tlobject.args - ) - else: - # If there were no arguments, we still need an - # on_response method, and hence "pass" if empty - builder.writeln('pass') - builder.end_block() + builder.end_block() # Write the __str__(self) and stringify(self) functions builder.writeln('def __str__(self):') @@ -549,9 +545,10 @@ class TLGenerator: return True # Something was written @staticmethod - def write_onresponse_code(builder, arg, args, name=None): + def write_read_code(builder, arg, args, name): """ - Writes the receive code for the given argument + Writes the read code for the given argument, setting the + arg.name variable to its read value. :param builder: The source code builder :param arg: The argument to write @@ -565,12 +562,17 @@ class TLGenerator: if arg.generic_definition: return # Do nothing, this only specifies a later type - if name is None: - name = 'self.{}'.format(arg.name) - # The argument may be a flag, only write that flag was given! was_flag = False if arg.is_flag: + # Treat 'true' flags as a special case, since they're true if + # they're set, and nothing else needs to actually be read. + if 'true' == arg.type: + builder.writeln( + '{} = bool(flags & {})'.format(name, 1 << arg.flag_index) + ) + return + was_flag = True builder.writeln('if flags & {}:'.format( 1 << arg.flag_index @@ -585,11 +587,10 @@ class TLGenerator: builder.writeln("reader.read_int()") builder.writeln('{} = []'.format(name)) - builder.writeln('_len = reader.read_int()') - builder.writeln('for _ in range(_len):') + builder.writeln('for _ in range(reader.read_int()):') # Temporary disable .is_vector, not to enter this if again arg.is_vector = False - TLGenerator.write_onresponse_code(builder, arg, args, name='_x') + TLGenerator.write_read_code(builder, arg, args, name='_x') builder.writeln('{}.append(_x)'.format(name)) arg.is_vector = True @@ -642,7 +643,10 @@ class TLGenerator: builder.end_block() if was_flag: - builder.end_block() + builder.current_indent -= 1 + builder.writeln('else:') + builder.writeln('{} = None'.format(name)) + builder.current_indent -= 1 # Restore .is_flag arg.is_flag = True From 7b5d409c49e3756d3f9b1d1199eee9c9923f17a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Sat, 7 Oct 2017 17:55:37 +0200 Subject: [PATCH 02/58] Warn users on .add_update_handler if no workers are running (#300) --- telethon/telegram_bare_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 17e8a364..cda8e2ab 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -1,6 +1,7 @@ import logging import os import threading +import warnings from datetime import timedelta, datetime from hashlib import md5 from io import BytesIO @@ -742,6 +743,9 @@ class TelegramBareClient: def add_update_handler(self, handler): """Adds an update handler (a function which takes a TLObject, an update, as its parameter) and listens for updates""" + if not self.updates.get_workers: + warnings.warn("There are no update workers running, so adding an update handler will have no effect.") + sync = not self.updates.handlers self.updates.handlers.append(handler) if sync: From 244a47cddd3ae9e7859afd73892e3647e44a3bca Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 7 Oct 2017 18:52:27 +0200 Subject: [PATCH 03/58] Fix consuming all retries on Requests returning False/empty list --- telethon/telegram_bare_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index cda8e2ab..b8d1b071 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -445,7 +445,7 @@ class TelegramBareClient: raise self._background_error result = self._invoke(sender, call_receive, *requests) - if result: + if result is not None: return result raise ValueError('Number of retries reached 0.') From 62aec947c02803ae020bdb5db6caf677647fcfc2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 8 Oct 2017 10:52:56 +0200 Subject: [PATCH 04/58] Chang auto-reconnect condition (fix #303) --- telethon/telegram_bare_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index b8d1b071..47d1e2ae 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -482,8 +482,8 @@ class TelegramBareClient: pass # We will just retry except ConnectionResetError: - if not self._authorized or self._reconnect_lock.locked(): - # Only attempt reconnecting if we're authorized and not + if not self._user_connected or self._reconnect_lock.locked(): + # Only attempt reconnecting if the user called connect and not # reconnecting already. raise From 83677fc927fdf65bb4bca5ed3257a2997f847db2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 8 Oct 2017 13:45:14 +0200 Subject: [PATCH 05/58] Enhance and use .get_input_photo on the generated code --- telethon/utils.py | 7 +++++-- telethon_generator/tl_generator.py | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index c4c3182c..9c3ee2fe 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -20,8 +20,8 @@ from .tl.types import ( GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty, InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable, InputMediaUploadedDocument, - InputMediaUploadedPhoto, - DocumentAttributeFilename) + InputMediaUploadedPhoto, DocumentAttributeFilename, photos +) def get_display_name(entity): @@ -188,6 +188,9 @@ def get_input_photo(photo): if type(photo).SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto') return photo + if isinstance(photo, photos.Photo): + photo = photo.photo + if isinstance(photo, Photo): return InputPhoto(id=photo.id, access_hash=photo.access_hash) diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 0fb2c1ed..0e4f0013 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -143,7 +143,7 @@ class TLGenerator: builder.writeln( 'from {}.utils import get_input_peer, ' 'get_input_channel, get_input_user, ' - 'get_input_media'.format('.' * depth) + 'get_input_media, get_input_photo'.format('.' * depth) ) # Import 'os' for those needing access to 'os.urandom()' @@ -402,6 +402,8 @@ class TLGenerator: TLGenerator.write_get_input(builder, arg, 'get_input_user') elif arg.type == 'InputMedia' and tlobject.is_function: TLGenerator.write_get_input(builder, arg, 'get_input_media') + elif arg.type == 'InputPhoto' and tlobject.is_function: + TLGenerator.write_get_input(builder, arg, 'get_input_photo') else: builder.writeln('self.{0} = {0}'.format(arg.name)) From 15e90dcb69f454e0b90a8267bd18bc8e5255a143 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 8 Oct 2017 16:15:30 +0200 Subject: [PATCH 06/58] Allow specifying a threshold to handle flood waits --- telethon/telegram_bare_client.py | 14 ++++++++++---- telethon/tl/session.py | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 47d1e2ae..b98ef976 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -534,10 +534,16 @@ class TelegramBareClient: '[ERROR] Telegram is having some internal issues', e ) - except FloodWaitError: - sender.disconnect() - self.disconnect() - raise + except FloodWaitError as e: + if e.seconds > self.session.flood_sleep_threshold | 0: + sender.disconnect() + self.disconnect() + raise + + self._logger.debug( + 'Sleep of %d seconds below threshold, sleeping' % e.seconds + ) + sleep(e.seconds) # Some really basic functionality diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 98ffda16..f597048f 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -37,6 +37,7 @@ class Session: self.lang_pack = session.lang_pack self.report_errors = session.report_errors self.save_entities = session.save_entities + self.flood_sleep_threshold = session.flood_sleep_threshold else: # str / None self.session_user_id = session_user_id @@ -50,6 +51,7 @@ class Session: self.lang_pack = '' self.report_errors = True self.save_entities = True + self.flood_sleep_threshold = 60 # Cross-thread safety self._seq_no_lock = Lock() From 48c8837f19ca4e37da3f932e6f95f4a661bb3d60 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 8 Oct 2017 16:25:50 +0200 Subject: [PATCH 07/58] Don't look on all dialogs on .get_entity miss --- telethon/telegram_client.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index bdaa41d8..66ea5ea3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -956,9 +956,17 @@ class TelegramClient(TelegramBareClient): ) if self.session.save_entities: - # Not found, look in the dialogs (this will save the users) - self.get_dialogs(limit=None) - + # Not found, look in the latest dialogs. + # This is useful if for instance someone just sent a message but + # the updates didn't specify who, as this person or chat should + # be in the latest dialogs. + self(GetDialogsRequest( + offset_date=None, + offset_id=0, + offset_peer=InputPeerEmpty(), + limit=0, + exclude_pinned=True + )) try: return self.session.entities.get_input_entity(peer) except KeyError: From 1f54cbfb5abed9766c51bff6501c9b602b500950 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 8 Oct 2017 17:51:29 +0200 Subject: [PATCH 08/58] Make str(TLObject) return valid code to generate it back --- telethon/tl/tlobject.py | 69 +++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index f34ed558..8deb59e3 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -1,3 +1,4 @@ +from datetime import datetime from threading import Event @@ -19,18 +20,14 @@ class TLObject: """ if indent is None: if isinstance(obj, TLObject): - children = obj.to_dict(recursive=False) - if children: - return '{}: {}'.format( - type(obj).__name__, TLObject.pretty_format(children) - ) - else: - return type(obj).__name__ + return '{}({})'.format(type(obj).__name__, ', '.join( + '{}={}'.format(k, TLObject.pretty_format(v)) + for k, v in obj.to_dict(recursive=False).items() + )) if isinstance(obj, dict): return '{{{}}}'.format(', '.join( - '{}: {}'.format( - k, TLObject.pretty_format(v) - ) for k, v in obj.items() + '{}: {}'.format(k, TLObject.pretty_format(v)) + for k, v in obj.items() )) elif isinstance(obj, str) or isinstance(obj, bytes): return repr(obj) @@ -38,31 +35,36 @@ class TLObject: return '[{}]'.format( ', '.join(TLObject.pretty_format(x) for x in obj) ) + elif isinstance(obj, datetime): + return 'datetime.fromtimestamp({})'.format(obj.timestamp()) else: - return str(obj) + return repr(obj) else: result = [] - if isinstance(obj, TLObject): - result.append(type(obj).__name__) - children = obj.to_dict(recursive=False) - if children: - result.append(': ') - result.append(TLObject.pretty_format( - obj.to_dict(recursive=False), indent - )) + if isinstance(obj, TLObject) or isinstance(obj, dict): + if isinstance(obj, dict): + d = obj + start, end, sep = '{', '}', ': ' + else: + d = obj.to_dict(recursive=False) + start, end, sep = '(', ')', '=' + result.append(type(obj).__name__) - elif isinstance(obj, dict): - result.append('{\n') - indent += 1 - for k, v in obj.items(): + result.append(start) + if d: + result.append('\n') + indent += 1 + for k, v in d.items(): + result.append('\t' * indent) + result.append(k) + result.append(sep) + result.append(TLObject.pretty_format(v, indent)) + result.append(',\n') + result.pop() # last ',\n' + indent -= 1 + result.append('\n') result.append('\t' * indent) - result.append(k) - result.append(': ') - result.append(TLObject.pretty_format(v, indent)) - result.append(',\n') - indent -= 1 - result.append('\t' * indent) - result.append('}') + result.append(end) elif isinstance(obj, str) or isinstance(obj, bytes): result.append(repr(obj)) @@ -78,8 +80,13 @@ class TLObject: result.append('\t' * indent) result.append(']') + elif isinstance(obj, datetime): + result.append('datetime.fromtimestamp(') + result.append(repr(obj.timestamp())) + result.append(')') + else: - result.append(str(obj)) + result.append(repr(obj)) return ''.join(result) From 4a482b35068def00c133374e9dabf269d7cd6868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Mon, 9 Oct 2017 09:54:48 +0200 Subject: [PATCH 09/58] .get_drafts() and a custom Draft class (#310) --- telethon/telegram_client.py | 24 +++++++--- telethon/tl/custom/__init__.py | 1 + telethon/tl/custom/draft.py | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 telethon/tl/custom/__init__.py create mode 100644 telethon/tl/custom/draft.py diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 66ea5ea3..5eda9f1a 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -15,6 +15,7 @@ from .errors import ( ) from .network import ConnectionMode from .tl import TLObject +from .tl.custom import Draft from .tl.entity_database import EntityDatabase from .tl.functions.account import ( GetPasswordRequest @@ -28,8 +29,8 @@ from .tl.functions.contacts import ( ) from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest, - SendMessageRequest, GetChatsRequest -) + SendMessageRequest, GetChatsRequest, + GetAllDraftsRequest) from .tl.functions import channels from .tl.functions import messages @@ -302,9 +303,20 @@ class TelegramClient(TelegramBareClient): [utils.find_user_or_chat(d.peer, entities, entities) for d in ds] ) - # endregion + def get_drafts(self): # TODO: Ability to provide a `filter` + """ + Gets all open draft messages. - # region Message requests + Returns a list of custom `Draft` objects that are easy to work with: You can call + `draft.set_message('text')` to change the message, or delete it through `draft.delete()`. + + :return List[telethon.tl.custom.Draft]: A list of open drafts + """ + response = self(GetAllDraftsRequest()) + self.session.process_entities(response) + self.session.generate_sequence(response.seq) + drafts = [Draft._from_update(self, u) for u in response.updates] + return drafts def send_message(self, entity, @@ -873,9 +885,9 @@ class TelegramClient(TelegramBareClient): pass if isinstance(entity, int) or ( - isinstance(entity, TLObject) and + isinstance(entity, TLObject) and # crc32(b'InputPeer') and crc32(b'Peer') - type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)): + type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)): ie = self.get_input_entity(entity) result = None if isinstance(ie, InputPeerUser): diff --git a/telethon/tl/custom/__init__.py b/telethon/tl/custom/__init__.py new file mode 100644 index 00000000..40914f16 --- /dev/null +++ b/telethon/tl/custom/__init__.py @@ -0,0 +1 @@ +from .draft import Draft diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py new file mode 100644 index 00000000..c50baa78 --- /dev/null +++ b/telethon/tl/custom/draft.py @@ -0,0 +1,80 @@ +from ..functions.messages import SaveDraftRequest +from ..types import UpdateDraftMessage + + +class Draft: + """ + Custom class that encapsulates a draft on the Telegram servers, providing + an abstraction to change the message conveniently. The library will return + instances of this class when calling `client.get_drafts()`. + """ + def __init__(self, client, peer, draft): + self._client = client + self._peer = peer + + self.text = draft.message + self.date = draft.date + self.no_webpage = draft.no_webpage + self.reply_to_msg_id = draft.reply_to_msg_id + self.entities = draft.entities + + @classmethod + def _from_update(cls, client, update): + if not isinstance(update, UpdateDraftMessage): + raise ValueError( + 'You can only create a new `Draft` from a corresponding ' + '`UpdateDraftMessage` object.' + ) + + return cls(client=client, peer=update.peer, draft=update.draft) + + @property + def entity(self): + return self._client.get_entity(self._peer) + + @property + def input_entity(self): + return self._client.get_input_entity(self._peer) + + def set_message(self, text, no_webpage=None, reply_to_msg_id=None, entities=None): + """ + Changes the draft message on the Telegram servers. The changes are + reflected in this object. Changing only individual attributes like for + example the `reply_to_msg_id` should be done by providing the current + values of this object, like so: + + draft.set_message( + draft.text, + no_webpage=draft.no_webpage, + reply_to_msg_id=NEW_VALUE, + entities=draft.entities + ) + + :param str text: New text of the draft + :param bool no_webpage: Whether to attach a web page preview + :param int reply_to_msg_id: Message id to reply to + :param list entities: A list of formatting entities + :return bool: `True` on success + """ + result = self._client(SaveDraftRequest( + peer=self._peer, + message=text, + no_webpage=no_webpage, + reply_to_msg_id=reply_to_msg_id, + entities=entities + )) + + if result: + self.text = text + self.no_webpage = no_webpage + self.reply_to_msg_id = reply_to_msg_id + self.entities = entities + + return result + + def delete(self): + """ + Deletes this draft + :return bool: `True` on success + """ + return self.set_message(text='') From 401de913af8bd19e88de0c78b027f7750b15a73f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 9 Oct 2017 10:59:15 +0200 Subject: [PATCH 10/58] Fix EntityDatabase failing with InputPeer keys --- telethon/tl/entity_database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index 61c07efc..00284409 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -148,8 +148,8 @@ class EntityDatabase: if isinstance(key, TLObject): sc = type(key).SUBCLASS_OF_ID - if sc == 0x2d45687: - # Subclass of "Peer" + if sc in {0x2d45687, 0xc91c90b6}: + # Subclass of "Peer" or "InputPeer" return self._entities[utils.get_peer_id(key, add_mark=True)] elif sc in {0x2da17977, 0xc5af5d94, 0x6d44b7db}: # Subclass of "User", "Chat" or "Channel" From 4673a02ce647f37830d6a7b4fb76ee3124d84afd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 9 Oct 2017 11:04:56 +0200 Subject: [PATCH 11/58] Stop calling .process_entities where not needed --- telethon/telegram_client.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5eda9f1a..fdd2c612 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -889,20 +889,19 @@ class TelegramClient(TelegramBareClient): # crc32(b'InputPeer') and crc32(b'Peer') type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)): ie = self.get_input_entity(entity) - result = None if isinstance(ie, InputPeerUser): - result = self(GetUsersRequest([ie])) + self(GetUsersRequest([ie])) elif isinstance(ie, InputPeerChat): - result = self(GetChatsRequest([ie.chat_id])) + self(GetChatsRequest([ie.chat_id])) elif isinstance(ie, InputPeerChannel): - result = self(GetChannelsRequest([ie])) - - if result: - self.session.process_entities(result) - try: - return self.session.entities[ie] - except KeyError: - pass + self(GetChannelsRequest([ie])) + try: + # session.process_entities has been called in the MtProtoSender + # with the result of these calls, so they should now be on the + # entities database. + return self.session.entities[ie] + except KeyError: + pass if isinstance(entity, str): return self._get_entity_from_string(entity) @@ -918,11 +917,11 @@ class TelegramClient(TelegramBareClient): phone = EntityDatabase.parse_phone(string) if phone: entity = phone - self.session.process_entities(self(GetContactsRequest(0))) + self(GetContactsRequest(0)) else: entity = string.strip('@').lower() - self.session.process_entities(self(ResolveUsernameRequest(entity))) - + self(ResolveUsernameRequest(entity)) + # MtProtoSender will call .process_entities on the requests made try: return self.session.entities[entity] except KeyError: From 2a1a4508b875bd15dabf7dc444ee5eadd86bfedb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 9 Oct 2017 11:20:09 +0200 Subject: [PATCH 12/58] Allow overriding DocumentAttributes on .send_file (fix #294) --- telethon/telegram_client.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index fdd2c612..ca24f5e7 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -500,9 +500,13 @@ class TelegramClient(TelegramBareClient): def send_file(self, entity, file, caption='', force_document=False, progress_callback=None, reply_to=None, + attributes=None, **kwargs): """Sends a file to the specified entity. The file may either be a path, a byte array, or a stream. + Note that if a byte array or a stream is given, a filename + or its type won't be inferred, and it will be sent as an + "unnamed application/octet-stream". An optional caption can also be specified for said file. @@ -519,6 +523,10 @@ class TelegramClient(TelegramBareClient): The "reply_to" parameter works exactly as the one on .send_message. + If "attributes" is set to be a list of DocumentAttribute's, these + will override the automatically inferred ones (so that you can + modify the file name of the file sent for instance). + If "is_voice_note" in kwargs, despite its value, and the file is sent as a document, it will be sent as a voice note. @@ -549,16 +557,28 @@ class TelegramClient(TelegramBareClient): # Determine mime-type and attributes # Take the first element by using [0] since it returns a tuple mime_type = guess_type(file)[0] - attributes = [ + attr_dict = { + DocumentAttributeFilename: DocumentAttributeFilename(os.path.basename(file)) # TODO If the input file is an audio, find out: # Performer and song title and add DocumentAttributeAudio - ] + } else: - attributes = [DocumentAttributeFilename('unnamed')] + attr_dict = { + DocumentAttributeFilename: + DocumentAttributeFilename('unnamed') + } if 'is_voice_note' in kwargs: - attributes.append(DocumentAttributeAudio(0, voice=True)) + attr_dict[DocumentAttributeAudio] = \ + DocumentAttributeAudio(0, voice=True) + + # Now override the attributes if any. As we have a dict of + # {cls: instance}, we can override any class with the list + # of attributes provided by the user easily. + if attributes: + for a in attributes: + attr_dict[type(a)] = a # Ensure we have a mime type, any; but it cannot be None # 'The "octet-stream" subtype is used to indicate that a body @@ -569,7 +589,7 @@ class TelegramClient(TelegramBareClient): media = InputMediaUploadedDocument( file=file_handle, mime_type=mime_type, - attributes=attributes, + attributes=list(attr_dict.values()), caption=caption ) From f984aae391b422dc81ae5ff9e79bbcfd63b77bc4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 9 Oct 2017 11:37:08 +0200 Subject: [PATCH 13/58] Except ProxyConnectionError on ReadThread (fix #307) --- telethon/telegram_bare_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index b98ef976..2447a67d 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -811,7 +811,8 @@ class TelegramBareClient: try: import socks - if isinstance(error, socks.GeneralProxyError): + if isinstance(error, socks.GeneralProxyError) or \ + isinstance(error, socks.ProxyConnectionError): # This is a known error, and it's not related to # Telegram but rather to the proxy. Disconnect and # hand it over to the main thread. From 6f1c05633e242bbd458a2f471ffa4c190f1f46d4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 9 Oct 2017 11:47:10 +0200 Subject: [PATCH 14/58] Join all threads when calling .disconnect() (fix #252) --- telethon/telegram_bare_client.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 2447a67d..bc56fd42 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -271,18 +271,16 @@ class TelegramBareClient: def disconnect(self): """Disconnects from the Telegram server and stops all the spawned threads""" - self._user_connected = False - self._recv_thread = None - - # Stop the workers from the background thread + self._user_connected = False # This will stop recv_thread's loop self.updates.stop_workers() - # This will trigger a "ConnectionResetError", for subsequent calls - # to read or send (from another thread) and usually, the background - # thread would try restarting the connection but since the - # ._recv_thread = None, it knows it doesn't have to. + # This will trigger a "ConnectionResetError" on the recv_thread, + # which won't attempt reconnecting as ._user_connected is False. self._sender.disconnect() + if self._recv_thread: + self._recv_thread.join() + # TODO Shall we clear the _exported_sessions, or may be reused? pass From a7622324dd39795fa028337c78e5d652eb2563f6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 9 Oct 2017 12:00:14 +0200 Subject: [PATCH 15/58] Remove unnecessary offset_index variable on .download_file --- telethon/telegram_bare_client.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index bc56fd42..d24f6635 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -688,10 +688,8 @@ class TelegramBareClient: cdn_decrypter = None try: - offset_index = 0 + offset = 0 while True: - offset = offset_index * part_size - try: if cdn_decrypter: result = cdn_decrypter.get_file() @@ -710,7 +708,7 @@ class TelegramBareClient: client = self._get_exported_client(e.new_dc) continue - offset_index += 1 + offset += part_size # If we have received no data (0 bytes), the file is over # So there is nothing left to download and write From e2ac18b7bc76df56fab8aeee23f93d622aeea51a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 9 Oct 2017 13:19:03 +0200 Subject: [PATCH 16/58] Use larger chunks when downloading/uploading files --- telethon/utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index 9c3ee2fe..9efc5ee5 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -378,11 +378,7 @@ def find_user_or_chat(peer, users, chats): def get_appropriated_part_size(file_size): """Gets the appropriated part size when uploading or downloading files, given an initial file size""" - if file_size <= 1048576: # 1MB - return 32 - if file_size <= 10485760: # 10MB - return 64 - if file_size <= 393216000: # 375MB + if file_size <= 104857600: # 100MB return 128 if file_size <= 786432000: # 750MB return 256 From db623e37fd48f7e182795f4a94ed4ca8fbd1ca3e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 9 Oct 2017 13:23:39 +0200 Subject: [PATCH 17/58] Except ConnectionResetError on ._reconnect (fix #309) --- telethon/telegram_bare_client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index d24f6635..781b7112 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -297,10 +297,13 @@ class TelegramBareClient: # Assume we are disconnected due to some error, so connect again with self._reconnect_lock: # Another thread may have connected again, so check that first - if not self.is_connected(): - return self.connect() - else: + if self.is_connected(): return True + + try: + return self.connect() + except ConnectionResetError: + return False else: self.disconnect() self.session.auth_key = None # Force creating new auth_key From da51e71def9d8e124bac686563ccfee033b1148f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 9 Oct 2017 19:40:39 +0200 Subject: [PATCH 18/58] Fix .get_input_entity returning marked IDs (closes #314) --- telethon/tl/entity_database.py | 6 ++++-- telethon/utils.py | 27 ++++++++++----------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index 00284409..a9f6332a 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -177,8 +177,10 @@ class EntityDatabase: def get_input_entity(self, peer): try: - i, k = utils.get_peer_id(peer, add_mark=True, get_kind=True) - h = self._input_entities[i] + i = utils.get_peer_id(peer, add_mark=True) + h = self._input_entities[i] # we store the IDs marked + i, k = utils.resolve_id(i) # removes the mark and returns kind + if k == PeerUser: return InputPeerUser(i, h) elif k == PeerChat: diff --git a/telethon/utils.py b/telethon/utils.py index 9efc5ee5..3fa84155 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -303,16 +303,14 @@ def get_input_media(media, user_caption=None, is_photo=False): _raise_cast_fail(media, 'InputMedia') -def get_peer_id(peer, add_mark=False, get_kind=False): +def get_peer_id(peer, add_mark=False): """Finds the ID of the given peer, and optionally converts it to the "bot api" format if 'add_mark' is set to True. - - If 'get_kind', the kind will be returned as a second value. """ # First we assert it's a Peer TLObject, or early return for integers if not isinstance(peer, TLObject): if isinstance(peer, int): - return (peer, PeerUser) if get_kind else peer + return peer else: _raise_cast_fail(peer, 'int') @@ -321,25 +319,20 @@ def get_peer_id(peer, add_mark=False, get_kind=False): peer = get_input_peer(peer, allow_self=False) # Set the right ID/kind, or raise if the TLObject is not recognised - i, k = None, None if isinstance(peer, PeerUser) or isinstance(peer, InputPeerUser): - i, k = peer.user_id, PeerUser + return peer.user_id elif isinstance(peer, PeerChat) or isinstance(peer, InputPeerChat): - i, k = peer.chat_id, PeerChat + return -peer.chat_id if add_mark else peer.chat_id elif isinstance(peer, PeerChannel) or isinstance(peer, InputPeerChannel): - i, k = peer.channel_id, PeerChannel - else: - _raise_cast_fail(peer, 'int') - - if add_mark: - if k == PeerChat: - i = -i - elif k == PeerChannel: + i = peer.channel_id + if add_mark: # Concat -100 through math tricks, .to_supergroup() on Madeline # IDs will be strictly positive -> log works - i = -(i + pow(10, math.floor(math.log10(i) + 3))) + return -(i + pow(10, math.floor(math.log10(i) + 3))) + else: + return i - return (i, k) if get_kind else i # return kind only if get_kind + _raise_cast_fail(peer, 'int') def resolve_id(marked_id): From 301da16f29462c2bac3581536aa6c416fbea5eb7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 11 Oct 2017 21:09:09 +0200 Subject: [PATCH 19/58] Fix pong response not reading all data from the buffer --- telethon/network/mtproto_sender.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index ebbd2a9a..01565251 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -11,7 +11,7 @@ from ..errors import ( from ..extensions import BinaryReader from ..tl import TLMessage, MessageContainer, GzipPacked from ..tl.all_tlobjects import tlobjects -from ..tl.types import MsgsAck +from ..tl.types import MsgsAck, Pong from ..tl.functions.auth import LogOutRequest logging.getLogger(__name__).addHandler(logging.NullHandler()) @@ -249,12 +249,13 @@ class MtProtoSender: def _handle_pong(self, msg_id, sequence, reader): self._logger.debug('Handling pong') - reader.read_int(signed=False) # code - received_msg_id = reader.read_long() + pong = reader.tgread_object() + assert isinstance(pong, Pong) - request = self._pop_request(received_msg_id) + request = self._pop_request(pong.msg_id) if request: self._logger.debug('Pong confirmed a request') + request.result = pong request.confirm_received.set() return True From a6c898f8d12bf60f1298784981060f238b5e0178 Mon Sep 17 00:00:00 2001 From: clfs Date: Wed, 11 Oct 2017 14:26:13 -0700 Subject: [PATCH 20/58] Update test for key generation via nonces (#323) Closes #321 --- telethon_tests/crypto_test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/telethon_tests/crypto_test.py b/telethon_tests/crypto_test.py index cec18084..e11704a4 100644 --- a/telethon_tests/crypto_test.py +++ b/telethon_tests/crypto_test.py @@ -107,17 +107,17 @@ class CryptoTests(unittest.TestCase): @staticmethod def test_generate_key_data_from_nonce(): - server_nonce = b'I am the server nonce.' - new_nonce = b'I am a new calculated nonce.' + server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little') + new_nonce = int.from_bytes(b'The new, calculated 32-bit nonce', byteorder='little') key, iv = utils.generate_key_data_from_nonce(server_nonce, new_nonce) - expected_key = b'?\xc4\xbd\xdf\rWU\x8a\xf5\x0f+V\xdc\x96up\x1d\xeeG\x00\x81|\x1eg\x8a\x8f{\xf0y\x80\xda\xde' - expected_iv = b'Q\x9dpZ\xb7\xdd\xcb\x82_\xfa\xf4\x90\xecn\x10\x9cD\xd2\x01\x8d\x83\xa0\xa4^\xb8\x91,\x7fI am' + expected_key = b'/\xaa\x7f\xa1\xfcs\xef\xa0\x99zh\x03M\xa4\x8e\xb4\xab\x0eE]b\x95|\xfe\xc0\xf8\x1f\xd4\xa0\xd4\xec\x91' + expected_iv = b'\xf7\xae\xe3\xc8+=\xc2\xb8\xd1\xe1\x1b\x0e\x10\x07\x9fn\x9e\xdc\x960\x05\xf9\xea\xee\x8b\xa1h The ' assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format( key, expected_key) - assert iv == expected_iv, 'Key ("{}") does not equal expected ("{}")'.format( - key, expected_iv) + assert iv == expected_iv, 'IV ("{}") does not equal expected ("{}")'.format( + iv, expected_iv) @staticmethod def test_fingerprint_from_key(): From 3a4662c3bfbdd141fef73f8447ff8dac2285b82c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 12 Oct 2017 16:02:34 +0200 Subject: [PATCH 21/58] Remove forgotten print call from authenticator.py --- telethon/network/authenticator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index 1081897a..78df5d87 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -124,7 +124,6 @@ def _do_authentication(connection): raise AssertionError(server_dh_inner) if server_dh_inner.nonce != res_pq.nonce: - print(server_dh_inner.nonce, res_pq.nonce) raise SecurityError('Invalid nonce in encrypted answer') if server_dh_inner.server_nonce != res_pq.server_nonce: From 0c1170ee61cc0962827ebcaad86513b8d2b8db87 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 12 Oct 2017 16:40:59 +0200 Subject: [PATCH 22/58] Replace hardcoded reads with TLObject's .read() --- telethon/network/mtproto_sender.py | 52 ++++++++++++++++-------------- telethon/tl/gzip_packed.py | 2 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 01565251..ef02c05a 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -11,7 +11,7 @@ from ..errors import ( from ..extensions import BinaryReader from ..tl import TLMessage, MessageContainer, GzipPacked from ..tl.all_tlobjects import tlobjects -from ..tl.types import MsgsAck, Pong +from ..tl.types import MsgsAck, Pong, BadServerSalt, BadMsgNotification from ..tl.functions.auth import LogOutRequest logging.getLogger(__name__).addHandler(logging.NullHandler()) @@ -180,24 +180,24 @@ class MtProtoSender: if code == 0xf35c6d01: # rpc_result, (response of an RPC call) return self._handle_rpc_result(msg_id, sequence, reader) - if code == 0x347773c5: # pong + if code == Pong.CONSTRUCTOR_ID: return self._handle_pong(msg_id, sequence, reader) - if code == 0x73f1f8dc: # msg_container + if code == MessageContainer.CONSTRUCTOR_ID: return self._handle_container(msg_id, sequence, reader, state) - if code == 0x3072cfa1: # gzip_packed + if code == GzipPacked.CONSTRUCTOR_ID: return self._handle_gzip_packed(msg_id, sequence, reader, state) - if code == 0xedab447b: # bad_server_salt + if code == BadServerSalt.CONSTRUCTOR_ID: return self._handle_bad_server_salt(msg_id, sequence, reader) - if code == 0xa7eff811: # bad_msg_notification + if code == BadMsgNotification.CONSTRUCTOR_ID: return self._handle_bad_msg_notification(msg_id, sequence, reader) - # msgs_ack, it may handle the request we wanted - if code == 0x62d6b459: + if code == MsgsAck.CONSTRUCTOR_ID: # may handle the request we wanted ack = reader.tgread_object() + assert isinstance(ack, MsgsAck) # Ignore every ack request *unless* when logging out, when it's # when it seems to only make sense. We also need to set a non-None # result since Telegram doesn't send the response for these. @@ -219,7 +219,12 @@ class MtProtoSender: return True - self._logger.debug('Unknown message: {}'.format(hex(code))) + self._logger.debug( + '[WARN] Unknown message: {}, data left in the buffer: {}' + .format( + hex(code), repr(reader.get_bytes()[reader.tell_position():]) + ) + ) return False # endregion @@ -279,14 +284,15 @@ class MtProtoSender: def _handle_bad_server_salt(self, msg_id, sequence, reader): self._logger.debug('Handling bad server salt') - reader.read_int(signed=False) # code - bad_msg_id = reader.read_long() - reader.read_int() # bad_msg_seq_no - reader.read_int() # error_code - new_salt = reader.read_long(signed=False) - self.session.salt = new_salt + bad_salt = reader.tgread_object() + assert isinstance(bad_salt, BadServerSalt) - request = self._pop_request(bad_msg_id) + # Our salt is unsigned, but the objects work with signed salts + self.session.salt = struct.unpack( + ' Date: Thu, 12 Oct 2017 17:58:37 +0200 Subject: [PATCH 23/58] Fix handle RpcResult not always returning a bool --- telethon/network/mtproto_sender.py | 35 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index ef02c05a..76ec5317 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -351,25 +351,26 @@ class MtProtoSender: # else TODO Where should this error be reported? # Read may be async. Can an error not-belong to a request? self._logger.debug('Read RPC error: %s', str(error)) - else: - if request: - self._logger.debug('Reading request response') - if inner_code == 0x3072cfa1: # GZip packed - unpacked_data = gzip.decompress(reader.tgread_bytes()) - with BinaryReader(unpacked_data) as compressed_reader: - request.on_response(compressed_reader) - else: - reader.seek(-4) - request.on_response(reader) + return True # All contents were read okay - self.session.process_entities(request.result) - request.confirm_received.set() - return True + elif request: + self._logger.debug('Reading request response') + if inner_code == 0x3072cfa1: # GZip packed + unpacked_data = gzip.decompress(reader.tgread_bytes()) + with BinaryReader(unpacked_data) as compressed_reader: + request.on_response(compressed_reader) else: - # If it's really a result for RPC from previous connection - # session, it will be skipped by the handle_container() - self._logger.debug('Lost request will be skipped.') - return False + reader.seek(-4) + request.on_response(reader) + + self.session.process_entities(request.result) + request.confirm_received.set() + return True + + # If it's really a result for RPC from previous connection + # session, it will be skipped by the handle_container() + self._logger.debug('Lost request will be skipped.') + return False def _handle_gzip_packed(self, msg_id, sequence, reader, state): self._logger.debug('Handling gzip packed data') From bff2e6981e946a915b7a468c80949e7d8cc2b413 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 12 Oct 2017 18:03:10 +0200 Subject: [PATCH 24/58] Fix ._pop_request_of_type failing on not-found requests --- telethon/network/mtproto_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 76ec5317..aba44dc2 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -244,7 +244,7 @@ class MtProtoSender: the given type, or returns None if it's not found/doesn't match. """ message = self._pending_receive.get(msg_id, None) - if isinstance(message.request, t): + if message and isinstance(message.request, t): return self._pending_receive.pop(msg_id).request def _clear_all_pending(self): From 59c61cab2f849cd22139c8d96533f6d14d8ae0af Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 12 Oct 2017 18:41:58 +0200 Subject: [PATCH 25/58] Replace int.from_bytes with struct.unpack for consistency --- telethon/network/connection.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/telethon/network/connection.py b/telethon/network/connection.py index 2500c0c1..fe04352f 100644 --- a/telethon/network/connection.py +++ b/telethon/network/connection.py @@ -141,28 +141,25 @@ class Connection: raise ValueError('Invalid connection mode specified: ' + str(self._mode)) def _recv_tcp_full(self): - packet_length_bytes = self.read(4) - packet_length = int.from_bytes(packet_length_bytes, 'little') + packet_len_seq = self.read(8) # 4 and 4 + packet_len, seq = struct.unpack('= 127: - length = int.from_bytes(self.read(3) + b'\0', 'little') + length = struct.unpack(' Date: Thu, 12 Oct 2017 18:52:04 +0200 Subject: [PATCH 26/58] Fix .tgread_object not seeking back on TypeNotFoundError --- telethon/extensions/binary_reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 747d18c8..2355c6a4 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -129,6 +129,7 @@ class BinaryReader: return False # If there was still no luck, give up + self.seek(-4) # Go back raise TypeNotFoundError(constructor_id) return clazz.from_reader(self) From 16a0cecf468000703f0f4aae7a63251c29f651e7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 12 Oct 2017 19:47:13 +0200 Subject: [PATCH 27/58] Fix EntityDatabase.__delitem__ --- telethon/tl/entity_database.py | 69 +++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index a9f6332a..d37fc314 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -123,6 +123,29 @@ class EntityDatabase: if phone: self._username_id[phone] = marked_id + def _parse_key(self, key): + """Parses the given string, integer or TLObject key into a + marked user ID ready for use on self._entities. Raises + ValueError if it cannot be parsed. + """ + if isinstance(key, str): + phone = EntityDatabase.parse_phone(key) + try: + if phone: + return self._phone_id[phone] + else: + return self._username_id[key.lstrip('@').lower()] + except KeyError as e: + raise ValueError() from e + + if isinstance(key, int): + return key # normal IDs are assumed users + + if isinstance(key, TLObject): + return utils.get_peer_id(key, add_mark=True) + + raise ValueError() + def __getitem__(self, key): """Accepts a digit only string as phone number, otherwise it's treated as an username. @@ -135,35 +158,29 @@ class EntityDatabase: its specific entity is retrieved as User, Chat or Channel. Note that megagroups are channels with .megagroup = True. """ - if isinstance(key, str): - phone = EntityDatabase.parse_phone(key) - if phone: - return self._phone_id[phone] - else: - key = key.lstrip('@').lower() - return self._entities[self._username_id[key]] - - if isinstance(key, int): - return self._entities[key] # normal IDs are assumed users - - if isinstance(key, TLObject): - sc = type(key).SUBCLASS_OF_ID - if sc in {0x2d45687, 0xc91c90b6}: - # Subclass of "Peer" or "InputPeer" - return self._entities[utils.get_peer_id(key, add_mark=True)] - elif sc in {0x2da17977, 0xc5af5d94, 0x6d44b7db}: - # Subclass of "User", "Chat" or "Channel" - return key - - raise KeyError(key) + try: + return self._entities[self._parse_key(key)] + except (ValueError, KeyError) as e: + raise KeyError(key) from e def __delitem__(self, key): - target = self[key] - del self._entities[key] - if getattr(target, 'username'): - del self._username_id[target.username] + try: + old = self._entities.pop(self._parse_key(key)) + # Try removing the username and phone (if pop didn't fail), + # since the entity may have no username or phone, just ignore + # errors. It should be there if we popped the entity correctly. + try: + del self._username_id[getattr(old, 'username', None)] + except KeyError: + pass - # TODO Allow search by name by tokenizing the input and return a list + try: + del self._phone_id[getattr(old, 'phone', None)] + except KeyError: + pass + + except (ValueError, KeyError) as e: + raise KeyError(key) from e @staticmethod def parse_phone(phone): From f2338e49aec347f74246a01c4e838ff534dce8bf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 12 Oct 2017 19:54:23 +0200 Subject: [PATCH 28/58] Allow using a callable key on EntityDatabase --- telethon/tl/entity_database.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index d37fc314..c772e665 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -125,8 +125,13 @@ class EntityDatabase: def _parse_key(self, key): """Parses the given string, integer or TLObject key into a - marked user ID ready for use on self._entities. Raises - ValueError if it cannot be parsed. + marked user ID ready for use on self._entities. + + If a callable key is given, the entity will be passed to the + function, and if it returns a true-like value, the marked ID + for such entity will be returned. + + Raises ValueError if it cannot be parsed. """ if isinstance(key, str): phone = EntityDatabase.parse_phone(key) @@ -144,20 +149,15 @@ class EntityDatabase: if isinstance(key, TLObject): return utils.get_peer_id(key, add_mark=True) + if callable(key): + for k, v in self._entities.items(): + if key(v): + return k + raise ValueError() def __getitem__(self, key): - """Accepts a digit only string as phone number, - otherwise it's treated as an username. - - If an integer is given, it's treated as the ID of the desired User. - The ID given won't try to be guessed as the ID of a chat or channel, - as there may be an user with that ID, and it would be unreliable. - - If a Peer is given (PeerUser, PeerChat, PeerChannel), - its specific entity is retrieved as User, Chat or Channel. - Note that megagroups are channels with .megagroup = True. - """ + """See the ._parse_key() docstring for possible values of the key""" try: return self._entities[self._parse_key(key)] except (ValueError, KeyError) as e: From bec5f9fb8961b903efce57672f68d5889cd125a1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 12 Oct 2017 21:09:09 +0200 Subject: [PATCH 29/58] Add stub methods for more server responses --- telethon/network/mtproto_sender.py | 38 +++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index aba44dc2..aa9c0a74 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -11,7 +11,10 @@ from ..errors import ( from ..extensions import BinaryReader from ..tl import TLMessage, MessageContainer, GzipPacked from ..tl.all_tlobjects import tlobjects -from ..tl.types import MsgsAck, Pong, BadServerSalt, BadMsgNotification +from ..tl.types import ( + MsgsAck, Pong, BadServerSalt, BadMsgNotification, + MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo +) from ..tl.functions.auth import LogOutRequest logging.getLogger(__name__).addHandler(logging.NullHandler()) @@ -195,6 +198,15 @@ class MtProtoSender: if code == BadMsgNotification.CONSTRUCTOR_ID: return self._handle_bad_msg_notification(msg_id, sequence, reader) + if code == MsgDetailedInfo.CONSTRUCTOR_ID: + return self._handle_msg_detailed_info(msg_id, sequence, reader) + + if code == MsgNewDetailedInfo.CONSTRUCTOR_ID: + return self._handle_msg_new_detailed_info(msg_id, sequence, reader) + + if code == NewSessionCreated.CONSTRUCTOR_ID: + return self._handle_new_session_created(msg_id, sequence, reader) + if code == MsgsAck.CONSTRUCTOR_ID: # may handle the request we wanted ack = reader.tgread_object() assert isinstance(ack, MsgsAck) @@ -323,6 +335,30 @@ class MtProtoSender: else: raise error + def _handle_msg_detailed_info(self, msg_id, sequence, reader): + msg_new = reader.tgread_object() + assert isinstance(msg_new, MsgDetailedInfo) + + # TODO For now, simply ack msg_new.answer_msg_id + # Relevant tdesktop source code: https://goo.gl/VvpCC6 + self._send_acknowledge(msg_new.answer_msg_id) + return True + + def _handle_msg_new_detailed_info(self, msg_id, sequence, reader): + msg_new = reader.tgread_object() + assert isinstance(msg_new, MsgNewDetailedInfo) + + # TODO For now, simply ack msg_new.answer_msg_id + # Relevant tdesktop source code: https://goo.gl/G7DPsR + self._send_acknowledge(msg_new.answer_msg_id) + return True + + def _handle_new_session_created(self, msg_id, sequence, reader): + new_session = reader.tgread_object() + assert isinstance(new_session, NewSessionCreated) + # TODO https://goo.gl/LMyN7A + return True + def _handle_rpc_result(self, msg_id, sequence, reader): self._logger.debug('Handling RPC result') reader.read_int(signed=False) # code From 9cf5506ee4ac1d69ce6331b7c23557f5b9e7f1e9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 13 Oct 2017 09:59:37 +0200 Subject: [PATCH 30/58] Don't .disconnect() on FloodWaitError Since other requests can still be invoked, it makes no sense to call .disconnect(). --- telethon/telegram_bare_client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 781b7112..e8e8b7fb 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -537,8 +537,6 @@ class TelegramBareClient: except FloodWaitError as e: if e.seconds > self.session.flood_sleep_threshold | 0: - sender.disconnect() - self.disconnect() raise self._logger.debug( From db63b5e39a539456f9167c12a13f45202d7f29a1 Mon Sep 17 00:00:00 2001 From: 88ee55 <88ee55@gmail.com> Date: Fri, 13 Oct 2017 13:53:36 +0500 Subject: [PATCH 31/58] Fix .send_message not expecting UpdateNewChannelMessage (#331) --- telethon/telegram_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ca24f5e7..c9032b6f 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -47,7 +47,7 @@ from .tl.types import ( InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty, Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, - UpdateNewMessage, UpdateShortSentMessage, + UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel) from .tl.types.messages import DialogsSlice @@ -359,7 +359,7 @@ class TelegramClient(TelegramBareClient): break for update in result.updates: - if isinstance(update, UpdateNewMessage): + if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)): if update.message.id == msg_id: return update.message From 4fd9d361f0b6e63922639e8769deabc51654198b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 13 Oct 2017 11:38:12 +0200 Subject: [PATCH 32/58] Replace redundant isinstance calls with a tuple parameter --- telethon/extensions/tcp_client.py | 2 +- telethon/telegram_bare_client.py | 5 +++-- telethon/tl/entity_database.py | 4 +--- telethon/update_state.py | 3 +-- telethon/utils.py | 26 +++++++++++--------------- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 6feb9841..5255513a 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -14,7 +14,7 @@ class TcpClient: if isinstance(timeout, timedelta): self.timeout = timeout.seconds - elif isinstance(timeout, int) or isinstance(timeout, float): + elif isinstance(timeout, (int, float)): self.timeout = float(timeout) else: raise ValueError('Invalid timeout type', type(timeout)) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index e8e8b7fb..6e1d9b55 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -808,8 +808,9 @@ class TelegramBareClient: try: import socks - if isinstance(error, socks.GeneralProxyError) or \ - isinstance(error, socks.ProxyConnectionError): + if isinstance(error, ( + socks.GeneralProxyError, socks.ProxyConnectionError + )): # This is a known error, and it's not related to # Telegram but rather to the proxy. Disconnect and # hand it over to the main thread. diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index c772e665..554e2a5a 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -74,9 +74,7 @@ class EntityDatabase: getattr(p, 'access_hash', 0) # chats won't have hash if self.enabled_full: - if isinstance(e, User) \ - or isinstance(e, Chat) \ - or isinstance(e, Channel): + if isinstance(e, (User, Chat, Channel)): new.append(e) except ValueError: pass diff --git a/telethon/update_state.py b/telethon/update_state.py index 995e3eb2..7560c7d3 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -149,8 +149,7 @@ class UpdateState: self._updates.append(update.update) self._updates_available.set() - elif isinstance(update, tl.Updates) or \ - isinstance(update, tl.UpdatesCombined): + elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): self._updates.extend(update.updates) self._updates_available.set() diff --git a/telethon/utils.py b/telethon/utils.py index 3fa84155..d8bfb89f 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -37,7 +37,7 @@ def get_display_name(entity): else: return '(No name)' - if isinstance(entity, Chat) or isinstance(entity, Channel): + if isinstance(entity, (Chat, Channel)): return entity.title return '(unknown)' @@ -50,8 +50,7 @@ def get_extension(media): """Gets the corresponding extension for any Telegram media""" # Photos are always compressed as .jpg by Telegram - if (isinstance(media, UserProfilePhoto) or isinstance(media, ChatPhoto) or - isinstance(media, MessageMediaPhoto)): + if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)): return '.jpg' # Documents will come with a mime type @@ -87,12 +86,10 @@ def get_input_peer(entity, allow_self=True): else: return InputPeerUser(entity.id, entity.access_hash) - if any(isinstance(entity, c) for c in ( - Chat, ChatEmpty, ChatForbidden)): + if isinstance(entity, (Chat, ChatEmpty, ChatForbidden)): return InputPeerChat(entity.id) - if any(isinstance(entity, c) for c in ( - Channel, ChannelForbidden)): + if isinstance(entity, (Channel, ChannelForbidden)): return InputPeerChannel(entity.id, entity.access_hash) # Less common cases @@ -122,7 +119,7 @@ def get_input_channel(entity): if type(entity).SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') return entity - if isinstance(entity, Channel) or isinstance(entity, ChannelForbidden): + if isinstance(entity, (Channel, ChannelForbidden)): return InputChannel(entity.id, entity.access_hash) if isinstance(entity, InputPeerChannel): @@ -266,7 +263,7 @@ def get_input_media(media, user_caption=None, is_photo=False): if isinstance(media, MessageMediaGame): return InputMediaGame(id=media.game.id) - if isinstance(media, ChatPhoto) or isinstance(media, UserProfilePhoto): + if isinstance(media, (ChatPhoto, UserProfilePhoto)): if isinstance(media.photo_big, FileLocationUnavailable): return get_input_media(media.photo_small, is_photo=True) else: @@ -291,10 +288,9 @@ def get_input_media(media, user_caption=None, is_photo=False): venue_id=media.venue_id ) - if any(isinstance(media, t) for t in ( + if isinstance(media, ( MessageMediaEmpty, MessageMediaUnsupported, - FileLocationUnavailable, ChatPhotoEmpty, - UserProfilePhotoEmpty)): + ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable)): return InputMediaEmpty() if isinstance(media, Message): @@ -319,11 +315,11 @@ def get_peer_id(peer, add_mark=False): peer = get_input_peer(peer, allow_self=False) # Set the right ID/kind, or raise if the TLObject is not recognised - if isinstance(peer, PeerUser) or isinstance(peer, InputPeerUser): + if isinstance(peer, (PeerUser, InputPeerUser)): return peer.user_id - elif isinstance(peer, PeerChat) or isinstance(peer, InputPeerChat): + elif isinstance(peer, (PeerChat, InputPeerChat)): return -peer.chat_id if add_mark else peer.chat_id - elif isinstance(peer, PeerChannel) or isinstance(peer, InputPeerChannel): + elif isinstance(peer, (PeerChannel, InputPeerChannel)): i = peer.channel_id if add_mark: # Concat -100 through math tricks, .to_supergroup() on Madeline From f4b8772a854c56317920ead8ddb2fa0853252406 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 14 Oct 2017 11:37:47 +0200 Subject: [PATCH 33/58] Temporary fix for abusive duplicated updates (closes #336) --- telethon/update_state.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/telethon/update_state.py b/telethon/update_state.py index 7560c7d3..8dd2ffad 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -1,4 +1,5 @@ import logging +import pickle from collections import deque from datetime import datetime from threading import RLock, Event, Thread @@ -27,6 +28,7 @@ class UpdateState: self._updates_lock = RLock() self._updates_available = Event() self._updates = deque() + self._latest_updates = deque(maxlen=10) self._logger = logging.getLogger(__name__) @@ -141,6 +143,26 @@ class UpdateState: self._state.pts = pts + # TODO There must be a better way to handle updates rather than + # keeping a queue with the latest updates only, and handling + # the 'pts' correctly should be enough. However some updates + # like UpdateUserStatus (even inside UpdateShort) will be called + # repeatedly very often if invoking anything inside an update + # handler. TODO Figure out why. + """ + client = TelegramClient('anon', api_id, api_hash, update_workers=1) + client.connect() + def handle(u): + client.get_me() + client.add_update_handler(handle) + input('Enter to exit.') + """ + data = pickle.dumps(update.to_dict()) + if data in self._latest_updates: + return # Duplicated too + + self._latest_updates.append(data) + if type(update).SUBCLASS_OF_ID == 0x8af52aac: # crc32(b'Updates') # Expand "Updates" into "Update", and pass these to callbacks. # Since .users and .chats have already been processed, we From 9907d763a867636244e29582a1a294d93a6a94b7 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Sat, 14 Oct 2017 12:50:48 +0300 Subject: [PATCH 34/58] Use peer as key instead top_message on .get_dialogs (fix #329) --- telethon/telegram_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c9032b6f..3a1ba20e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -253,7 +253,7 @@ class TelegramClient(TelegramBareClient): if limit is None: limit = float('inf') - dialogs = {} # Use Dialog.top_message as identifier to avoid dupes + dialogs = {} # Use peer id as identifier to avoid dupes messages = {} # Used later for sorting TODO also return these? entities = {} while len(dialogs) < limit: @@ -268,7 +268,7 @@ class TelegramClient(TelegramBareClient): break for d in r.dialogs: - dialogs[d.top_message] = d + dialogs[utils.get_peer_id(d.peer, True)] = d for m in r.messages: messages[m.id] = m From 280a7006557cad141dc6b99be7fe583168eb6b10 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 14 Oct 2017 12:02:30 +0200 Subject: [PATCH 35/58] Attempt at not calling .connect for every file chunk --- telethon/telegram_bare_client.py | 34 +++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 6e1d9b55..5b65ea1c 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -416,11 +416,25 @@ class TelegramBareClient: # region Invoking Telegram requests - def __call__(self, *requests, retries=5): + def _get_sender(self, on_main_thread=None): + """Gets the appropriated sender based on the current thread""" + if on_main_thread is None: + on_main_thread = threading.get_ident() == self._main_thread_ident + + if on_main_thread or self._on_read_thread(): + sender = self._sender + else: + sender = self._sender.clone() + sender.connect() + return sender + + def __call__(self, *requests, retries=5, sender=None): """Invokes (sends) a MTProtoRequest and returns (receives) its result. The invoke will be retried up to 'retries' times before raising ValueError(). + + If 'sender' is not None, it will override the sender to be used. """ if not all(isinstance(x, TLObject) and x.content_related for x in requests): @@ -428,11 +442,7 @@ class TelegramBareClient: # Determine the sender to be used (main or a new connection) on_main_thread = threading.get_ident() == self._main_thread_ident - if on_main_thread or self._on_read_thread(): - sender = self._sender - else: - sender = self._sender.clone() - sender.connect() + sender = sender or self._get_sender(on_main_thread=on_main_thread) # We should call receive from this thread if there's no background # thread reading or if the server disconnected us and we're trying @@ -686,6 +696,10 @@ class TelegramBareClient: # The used client will change if FileMigrateError occurs client = self + # TODO Keeping just another variable for a sender feels messy, improve. + # This is done not to call .connect() for every single piece of the + # file we'll be trying to download, if we were from another thread. + sender = self._get_sender() cdn_decrypter = None try: @@ -697,7 +711,7 @@ class TelegramBareClient: else: result = client(GetFileRequest( input_location, offset, part_size - )) + ), sender=sender) if isinstance(result, FileCdnRedirect): cdn_decrypter, result = \ @@ -706,7 +720,11 @@ class TelegramBareClient: ) except FileMigrateError as e: + if sender != self._sender: + sender.disconnect() client = self._get_exported_client(e.new_dc) + # Client connected on this thread -> uses the right sender + sender = None continue offset += part_size @@ -721,6 +739,8 @@ class TelegramBareClient: if progress_callback: progress_callback(f.tell(), file_size) finally: + if sender != self._sender: + sender.disconnect() if client != self: client.disconnect() From d92e8e11add3cc61ddb5fb40676ad9596a786ec5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 14 Oct 2017 12:05:28 +0200 Subject: [PATCH 36/58] Update to v0.15.2 --- telethon/telegram_bare_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 5b65ea1c..fc89d12f 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -58,7 +58,7 @@ class TelegramBareClient: """ # Current TelegramClient version - __version__ = '0.15.1' + __version__ = '0.15.2' # TODO Make this thread-safe, all connections share the same DC _dc_options = None From 27728be2425d08b97e01e40058c1d7f5cc9a51fa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 15 Oct 2017 11:05:56 +0200 Subject: [PATCH 37/58] Revert "Attempt at not calling .connect for every file chunk" This reverts commit 280a7006557cad141dc6b99be7fe583168eb6b10. The reason for this is that it was causing a lot of files to be downloaded corrupted for some reason. This should be revisited to avoid creating a new connection for every chunk. --- telethon/telegram_bare_client.py | 34 +++++++------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index fc89d12f..98ddf6d0 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -416,25 +416,11 @@ class TelegramBareClient: # region Invoking Telegram requests - def _get_sender(self, on_main_thread=None): - """Gets the appropriated sender based on the current thread""" - if on_main_thread is None: - on_main_thread = threading.get_ident() == self._main_thread_ident - - if on_main_thread or self._on_read_thread(): - sender = self._sender - else: - sender = self._sender.clone() - sender.connect() - return sender - - def __call__(self, *requests, retries=5, sender=None): + def __call__(self, *requests, retries=5): """Invokes (sends) a MTProtoRequest and returns (receives) its result. The invoke will be retried up to 'retries' times before raising ValueError(). - - If 'sender' is not None, it will override the sender to be used. """ if not all(isinstance(x, TLObject) and x.content_related for x in requests): @@ -442,7 +428,11 @@ class TelegramBareClient: # Determine the sender to be used (main or a new connection) on_main_thread = threading.get_ident() == self._main_thread_ident - sender = sender or self._get_sender(on_main_thread=on_main_thread) + if on_main_thread or self._on_read_thread(): + sender = self._sender + else: + sender = self._sender.clone() + sender.connect() # We should call receive from this thread if there's no background # thread reading or if the server disconnected us and we're trying @@ -696,10 +686,6 @@ class TelegramBareClient: # The used client will change if FileMigrateError occurs client = self - # TODO Keeping just another variable for a sender feels messy, improve. - # This is done not to call .connect() for every single piece of the - # file we'll be trying to download, if we were from another thread. - sender = self._get_sender() cdn_decrypter = None try: @@ -711,7 +697,7 @@ class TelegramBareClient: else: result = client(GetFileRequest( input_location, offset, part_size - ), sender=sender) + )) if isinstance(result, FileCdnRedirect): cdn_decrypter, result = \ @@ -720,11 +706,7 @@ class TelegramBareClient: ) except FileMigrateError as e: - if sender != self._sender: - sender.disconnect() client = self._get_exported_client(e.new_dc) - # Client connected on this thread -> uses the right sender - sender = None continue offset += part_size @@ -739,8 +721,6 @@ class TelegramBareClient: if progress_callback: progress_callback(f.tell(), file_size) finally: - if sender != self._sender: - sender.disconnect() if client != self: client.disconnect() From 1b71c6fbf162b85522c82661685f3664376fc4ec Mon Sep 17 00:00:00 2001 From: Viktor Oreshkin Date: Mon, 16 Oct 2017 20:19:16 +0300 Subject: [PATCH 38/58] Fix vector regex in parser (#347) --- telethon_generator/parser/tl_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon_generator/parser/tl_object.py b/telethon_generator/parser/tl_object.py index 416bc587..51fe3d24 100644 --- a/telethon_generator/parser/tl_object.py +++ b/telethon_generator/parser/tl_object.py @@ -222,7 +222,7 @@ class TLArg: self.type = flag_match.group(2) # Then check if the type is a Vector - vector_match = re.match(r'vector<(\w+)>', self.type, re.IGNORECASE) + vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type) if vector_match: self.is_vector = True From ee01724cdb7027c1e38625d31446ba1ea7bade92 Mon Sep 17 00:00:00 2001 From: Viktor Oreshkin Date: Mon, 16 Oct 2017 21:15:22 +0300 Subject: [PATCH 39/58] Fix parsing for constructors and not objects (#348) --- telethon_generator/parser/tl_object.py | 15 +++++++++++++-- telethon_generator/tl_generator.py | 11 +++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/telethon_generator/parser/tl_object.py b/telethon_generator/parser/tl_object.py index 51fe3d24..79b4385d 100644 --- a/telethon_generator/parser/tl_object.py +++ b/telethon_generator/parser/tl_object.py @@ -98,12 +98,17 @@ class TLObject: def class_name(self): """Gets the class name following the Python style guidelines""" + return self.class_name_for(self.name, self.is_function) + @staticmethod + def class_name_for(typename, is_function=False): + """Gets the class name following the Python style guidelines""" # Courtesy of http://stackoverflow.com/a/31531797/4759433 - result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), self.name) + result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), + typename) result = result[:1].upper() + result[1:].replace('_', '') # If it's a function, let it end with "Request" to identify them - if self.is_function: + if is_function: result += 'Request' return result @@ -192,6 +197,7 @@ class TLArg: # Default values self.is_vector = False self.is_flag = False + self.skip_constructor_id = False self.flag_index = -1 # Special case: some types can be inferred, which makes it @@ -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..754866bb 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. @@ -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: From ed77ba6f8ff115ac624f02f691c9991e5b37be60 Mon Sep 17 00:00:00 2001 From: Andrey Egorov Date: Tue, 17 Oct 2017 01:39:04 +0300 Subject: [PATCH 40/58] Likely fix .log_out crashing "calling Event" (#349) --- telethon/network/mtproto_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index aa9c0a74..520cadbf 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -217,7 +217,7 @@ class MtProtoSender: r = self._pop_request_of_type(msg_id, LogOutRequest) if r: r.result = True # Telegram won't send this value - r.confirm_received() + r.confirm_received.set() self._logger.debug('Message ack confirmed', r) return True From 63dfb1e3ead5335e05bf30f96bfba146e5f529d8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 17 Oct 2017 10:15:13 +0200 Subject: [PATCH 41/58] Fix processing messages pending of acknowledge many times --- telethon/network/mtproto_sender.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 520cadbf..16a82bbd 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -174,6 +174,15 @@ class MtProtoSender: """ # TODO Check salt, session_id and sequence_number + if msg_id in self._need_confirmation: + # We're yet to acknowledge this message already, so just drop it + # as we are already aware of it. TODO Should we force acknowledging + # all the self._need_confirmation IDs? + self._logger.debug( + 'Ignoring message pending of acknowledge: {}'.format(msg_id) + ) + return False + self._need_confirmation.append(msg_id) code = reader.read_int(signed=False) From adb79b21cf08ae876ed70e2decb919051cce00c5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 17 Oct 2017 19:54:24 +0200 Subject: [PATCH 42/58] Replace .to_bytes() with the special .__bytes__ function --- telethon/network/authenticator.py | 14 +++++++------- telethon/network/mtproto_sender.py | 2 +- telethon/tl/gzip_packed.py | 8 ++++---- telethon/tl/message_container.py | 4 ++-- telethon/tl/tl_message.py | 2 +- telethon/tl/tlobject.py | 2 +- telethon_generator/tl_generator.py | 12 ++++++------ 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index 78df5d87..1accf493 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -42,7 +42,7 @@ def _do_authentication(connection): req_pq_request = ReqPqRequest( nonce=int.from_bytes(os.urandom(16), 'big', signed=True) ) - sender.send(req_pq_request.to_bytes()) + sender.send(bytes(req_pq_request)) with BinaryReader(sender.receive()) as reader: req_pq_request.on_response(reader) @@ -60,12 +60,12 @@ def _do_authentication(connection): p, q = rsa.get_byte_array(min(p, q)), rsa.get_byte_array(max(p, q)) new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True) - pq_inner_data = PQInnerData( + pq_inner_data = bytes(PQInnerData( pq=rsa.get_byte_array(pq), p=p, q=q, nonce=res_pq.nonce, server_nonce=res_pq.server_nonce, new_nonce=new_nonce - ).to_bytes() + )) # sha_digest + data + random_bytes cipher_text, target_fingerprint = None, None @@ -90,7 +90,7 @@ def _do_authentication(connection): public_key_fingerprint=target_fingerprint, encrypted_data=cipher_text ) - sender.send(req_dh_params.to_bytes()) + sender.send(bytes(req_dh_params)) # Step 2 response: DH Exchange with BinaryReader(sender.receive()) as reader: @@ -138,12 +138,12 @@ def _do_authentication(connection): gab = pow(g_a, b, dh_prime) # Prepare client DH Inner Data - client_dh_inner = ClientDHInnerData( + client_dh_inner = bytes(ClientDHInnerData( nonce=res_pq.nonce, server_nonce=res_pq.server_nonce, retry_id=0, # TODO Actual retry ID g_b=rsa.get_byte_array(gb) - ).to_bytes() + )) client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner @@ -156,7 +156,7 @@ def _do_authentication(connection): server_nonce=res_pq.server_nonce, encrypted_data=client_dh_encrypted, ) - sender.send(set_client_dh.to_bytes()) + sender.send(bytes(set_client_dh)) # Step 3 response: Complete DH Exchange with BinaryReader(sender.receive()) as reader: diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 16a82bbd..fab8bebd 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -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(' Date: Wed, 18 Oct 2017 12:17:13 +0200 Subject: [PATCH 43/58] Avoid calling .sync_updates when not needed --- telethon/telegram_bare_client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 98ddf6d0..b98dbeea 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -132,7 +132,7 @@ class TelegramBareClient: self._user_connected = False # Save whether the user is authorized here (a.k.a. logged in) - self._authorized = False + self._authorized = None # None = We don't know yet # Uploaded files cache so subsequent calls are instant self._upload_cache = {} @@ -223,7 +223,7 @@ class TelegramBareClient: # another data center and this would raise UserMigrateError) # to also assert whether the user is logged in or not. self._user_connected = True - if _sync_updates and not _cdn: + if self._authorized is None and _sync_updates and not _cdn: try: self.sync_updates() self._set_connected_and_authorized() @@ -749,10 +749,7 @@ class TelegramBareClient: if not self.updates.get_workers: warnings.warn("There are no update workers running, so adding an update handler will have no effect.") - sync = not self.updates.handlers self.updates.handlers.append(handler) - if sync: - self.sync_updates() def remove_update_handler(self, handler): self.updates.handlers.remove(handler) From 87dc476daeb6a13a6e459f74d33ab2f6ff440ff6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 18 Oct 2017 14:45:08 +0200 Subject: [PATCH 44/58] Fix ReadThread not starting on reconnect if already authorized --- telethon/telegram_bare_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index b98dbeea..65c63f4b 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -229,6 +229,8 @@ class TelegramBareClient: self._set_connected_and_authorized() except UnauthorizedError: self._authorized = False + elif self._authorized: + self._set_connected_and_authorized() return True From 16cf94c9add5e94d70c4eee2ac142d8e76af48b9 Mon Sep 17 00:00:00 2001 From: Andrey Egorov Date: Wed, 18 Oct 2017 15:47:03 +0300 Subject: [PATCH 45/58] Fix ._clear_all_pending failing due to a wrong call (#352) --- telethon/network/mtproto_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index fab8bebd..f948946f 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -270,7 +270,7 @@ class MtProtoSender: def _clear_all_pending(self): for r in self._pending_receive.values(): - r.confirm_received.set() + r.confirm_received.request.set() self._pending_receive.clear() def _handle_pong(self, msg_id, sequence, reader): From e349910eb9019a64dbf0874f114fdcbf0c955a81 Mon Sep 17 00:00:00 2001 From: Andrey Egorov Date: Wed, 18 Oct 2017 16:34:04 +0300 Subject: [PATCH 46/58] Fix attribute access order being swapped (#353) --- telethon/network/mtproto_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index f948946f..2e1f4627 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -270,7 +270,7 @@ class MtProtoSender: def _clear_all_pending(self): for r in self._pending_receive.values(): - r.confirm_received.request.set() + r.request.confirm_received.set() self._pending_receive.clear() def _handle_pong(self, msg_id, sequence, reader): From f49208f96179fb775e9d32c23bb2ba66bc6a5935 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 18 Oct 2017 20:43:46 +0200 Subject: [PATCH 47/58] Fix assert condition on generated code with flags involved The specific case was SendMessageRequest with InputMessageEntityMentionName, failing with bot/bot_info --- telethon_generator/tl_generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index fde3cd22..5ee12969 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -314,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) ) From 0e1249c8337d1aa61a0e23875fe8fcfd370e4b95 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 19 Oct 2017 10:42:09 +0200 Subject: [PATCH 48/58] Fix incorrectly generated code --- telethon_generator/tl_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 5ee12969..8fc6bb2d 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -318,7 +318,7 @@ class TLGenerator: cnd2 = ('not self.{}'.format(a.name) for a in ra) builder.writeln( "assert ({}) or ({}), '{} parameters must all " - "be False-y (like None) or all me True-y''".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) ) From 050cd95d324ba0f12fad90aaa564940a98312453 Mon Sep 17 00:00:00 2001 From: Andrey Egorov Date: Thu, 19 Oct 2017 11:51:34 +0300 Subject: [PATCH 49/58] Remove unnecessary .disconnect() from ._invoke() (#356) --- telethon/telegram_bare_client.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 65c63f4b..0b31c4c5 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -499,10 +499,7 @@ class TelegramBareClient: else: while self._user_connected and not self._reconnect(): sleep(0.1) # Retry forever until we can send the request - - finally: - if sender != self._sender: - sender.disconnect() + return None try: raise next(x.rpc_error for x in requests if x.rpc_error) From f37b9ed20eed6952357e8776b29217e547c21c8d Mon Sep 17 00:00:00 2001 From: Andrey Egorov Date: Fri, 20 Oct 2017 17:48:54 +0300 Subject: [PATCH 50/58] Fix new salt not being saved to session file (#362) --- telethon/network/mtproto_sender.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 2e1f4627..61e60484 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -312,6 +312,7 @@ class MtProtoSender: self.session.salt = struct.unpack( ' Date: Fri, 20 Oct 2017 15:44:43 +0200 Subject: [PATCH 51/58] Generate errors from PWRTelegram's API --- .gitignore | 1 + telethon/errors/__init__.py | 45 +- .../{rpc_errors.py => rpc_base_errors.py} | 0 telethon/errors/rpc_errors_303.py | 51 -- telethon/errors/rpc_errors_400.py | 453 ------------------ telethon/errors/rpc_errors_401.py | 98 ---- telethon/errors/rpc_errors_420.py | 16 - telethon_generator/error_descriptions | 65 +++ telethon_generator/error_generator.py | 172 +++++++ telethon_generator/errors.json | 1 + 10 files changed, 260 insertions(+), 642 deletions(-) rename telethon/errors/{rpc_errors.py => rpc_base_errors.py} (100%) delete mode 100644 telethon/errors/rpc_errors_303.py delete mode 100644 telethon/errors/rpc_errors_400.py delete mode 100644 telethon/errors/rpc_errors_401.py delete mode 100644 telethon/errors/rpc_errors_420.py create mode 100644 telethon_generator/error_descriptions create mode 100644 telethon_generator/error_generator.py create mode 100644 telethon_generator/errors.json diff --git a/.gitignore b/.gitignore index aef0b91f..dd5de4c3 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ telethon/tl/functions/ telethon/tl/types/ telethon/tl/all_tlobjects.py +telethon/tl/errors/rpc_error_list.py # User session *.session 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_generator/error_descriptions b/telethon_generator/error_descriptions new file mode 100644 index 00000000..f0a14e68 --- /dev/null +++ b/telethon_generator/error_descriptions @@ -0,0 +1,65 @@ +# These are comments. Spaces around the = are optional. Empty lines ignored. +#CODE=Human readable description + +FILE_MIGRATE_X=The file to be accessed is currently stored in DC {} +PHONE_MIGRATE_X=The phone number a user is trying to use for authorization is associated with DC {} +NETWORK_MIGRATE_X=The source IP address is associated with DC {} +USER_MIGRATE_X=The user whose identity is being used to execute queries is associated with DC {} +API_ID_INVALID=The api_id/api_hash combination is invalid +BOT_METHOD_INVALID=The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot +CDN_METHOD_INVALID=This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods +CHANNEL_INVALID=Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited +CHANNEL_PRIVATE=The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it +CHAT_ADMIN_REQUIRED=Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours) +CHAT_ID_INVALID=Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead +CONNECTION_LANG_PACK_INVALID=The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty +CONNECTION_LAYER_INVALID=The very first request must always be InvokeWithLayerRequest +DC_ID_INVALID=This occurs when an authorization is tried to be exported for the same data center one is currently connected to +FIELD_NAME_EMPTY=The field with the name FIELD_NAME is missing +FIELD_NAME_INVALID=The field with the name FIELD_NAME is invalid +FILE_PARTS_INVALID=The number of file parts is invalid +FILE_PART_X_MISSING=Part {} of the file is missing from storage +FILE_PART_INVALID=The file part number is invalid +FIRSTNAME_INVALID=The first name is invalid +INPUT_METHOD_INVALID=The invoked method does not exist anymore or has never existed +INPUT_REQUEST_TOO_LONG=The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (likeappending the vector constructor code at the end of a message) +LASTNAME_INVALID=The last name is invalid +LIMIT_INVALID=An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files +LOCATION_INVALID=The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files +MD5_CHECKSUM_INVALID=The MD5 check-sums do not match +MESSAGE_EMPTY=Empty or invalid UTF-8 message was sent +MESSAGE_ID_INVALID=The specified message ID is invalid +MESSAGE_TOO_LONG=Message was too long. Current maximum length is 4096 UTF-8 characters +MESSAGE_NOT_MODIFIED=Content of the message was not modified +MSG_WAIT_FAILED=A waiting call returned an error +OFFSET_INVALID=The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files +PASSWORD_HASH_INVALID=The password (and thus its hash value) you entered is invalid +PEER_ID_INVALID=An invalid Peer was used. Make sure to pass the right peer type +PHONE_CODE_EMPTY=The phone code is missing +PHONE_CODE_EXPIRED=The confirmation code has expired +PHONE_CODE_HASH_EMPTY=The phone code hash is missing +PHONE_CODE_INVALID=The phone code entered was invalid +PHONE_NUMBER_BANNED=The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam +PHONE_NUMBER_INVALID=The phone number is invalid +PHONE_NUMBER_OCCUPIED=The phone number is already in use +PHONE_NUMBER_UNOCCUPIED=The phone number is not yet being used +PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid +TYPE_CONSTRUCTOR_INVALID=The type constructor is invalid +USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{4,31}" +USERNAME_NOT_MODIFIED=The username is not different from the current username +USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet +USERNAME_OCCUPIED=The username is already taken +USERS_TOO_FEW=Not enough users (to create a chat, for example) +USERS_TOO_MUCH=The maximum number of users has been exceeded (to create a chat, for example) +USER_ID_INVALID=Invalid object ID for an user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited +ACTIVE_USER_REQUIRED=The method is only available to already activated users +AUTH_KEY_INVALID=The key is invalid +AUTH_KEY_PERM_EMPTY=The method is unavailable for temporary authorization key, not bound to permanent +AUTH_KEY_UNREGISTERED=The key is not registered in the system +INVITE_HASH_EXPIRED=The chat the user tried to join has expired and is not valid anymore +SESSION_EXPIRED=The authorization has expired +SESSION_PASSWORD_NEEDED=Two-steps verification is enabled and a password is required +SESSION_REVOKED=The authorization has been invalidated, because of the user terminating all sessions +USER_ALREADY_PARTICIPANT=The authenticated user is already a participant of the chat +USER_DEACTIVATED=The user has been deleted/deactivated +FLOOD_WAIT_X=A wait of {} seconds is required diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py new file mode 100644 index 00000000..fb5d0ef7 --- /dev/null +++ b/telethon_generator/error_generator.py @@ -0,0 +1,172 @@ +import json +import re +import urllib.request +from collections import defaultdict + +URL = 'https://rpc.pwrtelegram.xyz/?all' +OUTPUT = '../telethon/errors/rpc_error_list.py' +JSON_OUTPUT = 'errors.json' + +known_base_classes = { + 303: 'InvalidDCError', + 400: 'BadRequestError', + 401: 'UnauthorizedError', + 403: 'ForbiddenError', + 404: 'NotFoundError', + 420: 'FloodError', + 500: 'ServerError', +} + +# The API doesn't return the code for some (vital) errors. They are +# all assumed to be 400, except these well-known ones that aren't. +known_codes = { + 'ACTIVE_USER_REQUIRED': 401, + 'AUTH_KEY_UNREGISTERED': 401, + 'USER_DEACTIVATED': 401 +} + + +def fetch_errors(url=URL, output=JSON_OUTPUT): + print('Opening a connection to', url, '...') + r = urllib.request.urlopen(url) + print('Checking response...') + data = json.loads( + r.read().decode(r.info().get_param('charset') or 'utf-8') + ) + if data.get('ok'): + print('Response was okay, saving data') + with open(output, 'w', encoding='utf-8') as f: + json.dump(data, f) + return True + else: + print('The data received was not okay:') + print(json.dumps(data, indent=4)) + return False + + +def get_class_name(error_code): + if isinstance(error_code, int): + return known_base_classes.get( + error_code, 'RPCError' + str(error_code).replace('-', 'Neg') + ) + + if 'FIRSTNAME' in error_code: + error_code = error_code.replace('FIRSTNAME', 'FIRST_NAME') + + result = re.sub( + r'_([a-z])', lambda m: m.group(1).upper(), error_code.lower() + ) + return result[:1].upper() + result[1:].replace('_', '') + 'Error' + + +def write_error(f, code, name, desc, capture_name): + f.write( + f'\n' + f'\n' + f'class {name}({get_class_name(code)}):\n' + f' def __init__(self, **kwargs):\n' + f' ' + ) + if capture_name: + f.write( + f"self.{capture_name} = int(kwargs.get('capture', 0))\n" + f" " + ) + f.write(f'super(Exception, self).__init__(self, {repr(desc)}') + if capture_name: + f.write(f'.format(self.{capture_name})') + f.write(')\n') + + +def generate_code(json_file=JSON_OUTPUT, output=OUTPUT): + with open(json_file, encoding='utf-8') as f: + data = json.load(f) + + errors = defaultdict(set) + # PWRTelegram's API doesn't return all errors, which we do need here. + # Add some special known-cases manually first. + errors[420].add('FLOOD_WAIT_X') + errors[401].update(( + 'AUTH_KEY_INVALID', 'SESSION_EXPIRED', 'SESSION_REVOKED' + )) + errors[303].update(( + 'FILE_MIGRATE_X', 'PHONE_MIGRATE_X', + 'NETWORK_MIGRATE_X', 'USER_MIGRATE_X' + )) + for error_code, method_errors in data['result'].items(): + for error_list in method_errors.values(): + for error in error_list: + errors[int(error_code)].add(re.sub('_\d+', '_X', error).upper()) + + # Some errors are in the human result, but not with a code. Assume code 400 + for error in data['human_result']: + if error[0] != '-' and not error.isdigit(): + error = re.sub('_\d+', '_X', error).upper() + if not any(error in es for es in errors.values()): + errors[known_codes.get(error, 400)].add(error) + + # Some error codes are not known, so create custom base classes if needed + needed_base_classes = [ + (e, get_class_name(e)) for e in errors if e not in known_base_classes + ] + + # Prefer the descriptions that are related with Telethon way of coding to + # those that PWRTelegram's API provides. + telethon_descriptions = {} + with open('error_descriptions', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + equal = line.index('=') + message, description = line[:equal], line[equal + 1:] + telethon_descriptions[message.rstrip()] = description.lstrip() + + # Names for the captures, or 'x' if unknown + capture_names = { + 'FloodWaitError': 'seconds', + 'FileMigrateError': 'new_dc', + 'NetworkMigrateError': 'new_dc', + 'PhoneMigrateError': 'new_dc', + 'UserMigrateError': 'new_dc', + 'FilePartMissingError': 'which' + } + + # Everything ready, generate the code + with open(output, 'w', encoding='utf-8') as f: + f.write( + f'from .rpc_base_errors import RPCError, BadMessageError, ' + f'{", ".join(known_base_classes.values())}\n' + ) + for code, cls in needed_base_classes: + f.write( + f'\n' + f'\n' + f'class {cls}(RPCError):\n' + f' code = {code}\n' + ) + + patterns = [] # Save this dictionary later in the generated code + for error_code, error_set in errors.items(): + for error in sorted(error_set): + description = telethon_descriptions.get( + error, '\n'.join(data['human_result'].get( + error, ['No description known.'] + )) + ) + has_captures = '_X' in error + if has_captures: + name = get_class_name(error.replace('_X', '')) + pattern = error.replace('_X', r'_(\d+)') + else: + name, pattern = get_class_name(error), error + + patterns.append((pattern, name)) + capture = capture_names.get(name, 'x') if has_captures else None + # TODO Some errors have the same name but different code, + # split this accross different files? + write_error(f, error_code, name, description, capture) + + f.write('\n\nrpc_errors_all = {\n') + for pattern, name in patterns: + f.write(f' {repr(pattern)}: {name},\n') + f.write('}\n') diff --git a/telethon_generator/errors.json b/telethon_generator/errors.json new file mode 100644 index 00000000..e807ff2d --- /dev/null +++ b/telethon_generator/errors.json @@ -0,0 +1 @@ +{"ok": true, "result": {"400": {"account.updateProfile": ["ABOUT_TOO_LONG", "FIRSTNAME_INVALID"], "auth.importBotAuthorization": ["ACCESS_TOKEN_EXPIRED", "ACCESS_TOKEN_INVALID", "API_ID_INVALID"], "auth.sendCode": ["API_ID_INVALID", "INPUT_REQUEST_TOO_LONG", "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN", "PHONE_NUMBER_BANNED", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_PASSWORD_PROTECTED"], "messages.setInlineBotResults": ["ARTICLE_TITLE_EMPTY", "BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "MESSAGE_EMPTY", "QUERY_ID_INVALID", "REPLY_MARKUP_INVALID", "RESULT_TYPE_INVALID", "SEND_MESSAGE_TYPE_INVALID", "START_PARAM_INVALID"], "auth.importAuthorization": ["AUTH_BYTES_INVALID", "USER_ID_INVALID"], "invokeWithLayer": ["AUTH_BYTES_INVALID", "CDN_METHOD_INVALID", "CONNECTION_API_ID_INVALID", "CONNECTION_LANG_PACK_INVALID", "INPUT_LAYER_INVALID"], "channels.inviteToChannel": ["BOT_GROUPS_BLOCKED", "BOTS_TOO_MUCH", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_USER_DEACTIVATED", "USER_BANNED_IN_CHANNEL", "USER_BLOCKED", "USER_BOT", "USER_ID_INVALID", "USER_KICKED", "USER_NOT_MUTUAL_CONTACT", "USERS_TOO_MUCH"], "messages.getInlineBotResults": ["BOT_INLINE_DISABLED", "BOT_INVALID"], "messages.startBot": ["BOT_INVALID", "PEER_ID_INVALID", "START_PARAM_EMPTY", "START_PARAM_INVALID"], "messages.uploadMedia": ["BOT_MISSING", "PEER_ID_INVALID"], "stickers.addStickerToSet": ["BOT_MISSING", "STICKERSET_INVALID"], "stickers.changeStickerPosition": ["BOT_MISSING", "STICKER_INVALID"], "stickers.createStickerSet": ["BOT_MISSING", "PACK_SHORT_NAME_INVALID", "PEER_ID_INVALID", "STICKERS_EMPTY", "USER_ID_INVALID"], "stickers.removeStickerFromSet": ["BOT_MISSING", "STICKER_INVALID"], "messages.sendMessage": ["BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "ENTITY_MENTION_USER_INVALID", "INPUT_USER_DEACTIVATED", "MESSAGE_EMPTY", "MESSAGE_TOO_LONG", "PEER_ID_INVALID", "REPLY_MARKUP_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "phone.acceptCall": ["CALL_ALREADY_ACCEPTED", "CALL_ALREADY_DECLINED", "CALL_PEER_INVALID", "CALL_PROTOCOL_FLAGS_INVALID"], "phone.discardCall": ["CALL_ALREADY_ACCEPTED", "CALL_PEER_INVALID"], "phone.confirmCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.receivedCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.saveCallDebug": ["CALL_PEER_INVALID", "DATA_JSON_INVALID"], "phone.setCallRating": ["CALL_PEER_INVALID"], "phone.requestCall": ["CALL_PROTOCOL_FLAGS_INVALID", "PARTICIPANT_VERSION_OUTDATED", "USER_ID_INVALID"], "updates.getDifference": ["CDN_METHOD_INVALID", "DATE_EMPTY", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID"], "upload.getCdnFileHashes": ["CDN_METHOD_INVALID", "RSA_DECRYPT_FAILED"], "channels.checkUsername": ["CHANNEL_INVALID", "CHAT_ID_INVALID", "USERNAME_INVALID"], "channels.deleteChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteUserHistory": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.editAbout": ["CHANNEL_INVALID", "CHAT_ABOUT_NOT_MODIFIED", "CHAT_ABOUT_TOO_LONG", "CHAT_ADMIN_REQUIRED"], "channels.editAdmin": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_CREATOR", "USER_ID_INVALID", "USER_NOT_MUTUAL_CONTACT"], "channels.editBanned": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "USER_ADMIN_INVALID", "USER_ID_INVALID"], "channels.editPhoto": ["CHANNEL_INVALID"], "channels.editTitle": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.exportInvite": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "INVITE_HASH_EXPIRED"], "channels.exportMessageLink": ["CHANNEL_INVALID"], "channels.getAdminLog": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.getChannels": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getFullChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "MESSAGE_IDS_EMPTY"], "channels.getParticipant": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_ID_INVALID", "USER_NOT_PARTICIPANT"], "channels.getParticipants": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_INVALID"], "channels.joinChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHANNELS_TOO_MUCH"], "channels.leaveChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "USER_CREATOR", "USER_NOT_PARTICIPANT"], "channels.readHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.readMessageContents": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.reportSpam": ["CHANNEL_INVALID"], "channels.setStickers": ["CHANNEL_INVALID", "PARTICIPANTS_TOO_FEW"], "channels.toggleInvites": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.toggleSignatures": ["CHANNEL_INVALID"], "channels.updatePinnedMessage": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.updateUsername": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USERNAME_INVALID", "USERNAME_OCCUPIED"], "messages.editMessage": ["CHANNEL_INVALID", "MESSAGE_EDIT_TIME_EXPIRED", "MESSAGE_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED", "PEER_ID_INVALID"], "messages.forwardMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "MEDIA_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_IDS_EMPTY", "PEER_ID_INVALID", "RANDOM_ID_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "messages.getHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerSettings": ["CHANNEL_INVALID", "PEER_ID_INVALID"], "messages.sendMedia": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "FILE_PART_0_MISSING", "FILE_PART_LENGTH_INVALID", "FILE_PARTS_INVALID", "INPUT_USER_DEACTIVATED", "MEDIA_CAPTION_TOO_LONG", "MEDIA_EMPTY", "PEER_ID_INVALID", "PHOTO_EXT_INVALID", "USER_IS_BLOCKED", "USER_IS_BOT", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "messages.setTyping": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "updates.getChannelDifference": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID", "RANGES_INVALID"], "messages.getMessagesViews": ["CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerDialogs": ["CHANNEL_PRIVATE", "PEER_ID_INVALID"], "messages.addChatUser": ["CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_ALREADY_PARTICIPANT", "USER_ID_INVALID", "USERS_TOO_MUCH"], "messages.discardEncryption": ["CHAT_ID_EMPTY", "ENCRYPTION_ALREADY_DECLINED", "ENCRYPTION_ID_INVALID"], "messages.acceptEncryption": ["CHAT_ID_INVALID", "ENCRYPTION_ALREADY_ACCEPTED", "ENCRYPTION_ALREADY_DECLINED"], "messages.deleteChatUser": ["CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_NOT_PARTICIPANT"], "messages.editChatAdmin": ["CHAT_ID_INVALID"], "messages.editChatPhoto": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.editChatTitle": ["CHAT_ID_INVALID"], "messages.exportChatInvite": ["CHAT_ID_INVALID"], "messages.forwardMessage": ["CHAT_ID_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID", "YOU_BLOCKED_USER"], "messages.getChats": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getFullChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.migrateChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.reportEncryptedSpam": ["CHAT_ID_INVALID"], "messages.sendEncrypted": ["CHAT_ID_INVALID", "DATA_INVALID", "MSG_WAIT_FAILED"], "messages.setEncryptedTyping": ["CHAT_ID_INVALID"], "messages.toggleChatAdmins": ["CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "channels.createChannel": ["CHAT_TITLE_EMPTY"], "auth.recoverPassword": ["CODE_EMPTY"], "account.confirmPhone": ["CODE_HASH_INVALID", "PHONE_CODE_EMPTY"], "initConnection": ["CONNECTION_LAYER_INVALID", "INPUT_FETCH_FAIL"], "contacts.block": ["CONTACT_ID_INVALID"], "contacts.deleteContact": ["CONTACT_ID_INVALID"], "contacts.unblock": ["CONTACT_ID_INVALID"], "messages.getBotCallbackAnswer": ["DATA_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID"], "messages.sendEncryptedService": ["DATA_INVALID", "MSG_WAIT_FAILED"], "auth.exportAuthorization": ["DC_ID_INVALID"], "messages.requestEncryption": ["DH_G_A_INVALID", "USER_ID_INVALID"], "auth.bindTempAuthKey": ["ENCRYPTED_MESSAGE_INVALID", "INPUT_REQUEST_TOO_LONG", "TEMP_AUTH_KEY_EMPTY"], "messages.setBotPrecheckoutResults": ["ERROR_TEXT_EMPTY"], "contacts.importCard": ["EXPORT_CARD_INVALID"], "upload.getFile": ["FILE_ID_INVALID", "LIMIT_INVALID", "LOCATION_INVALID", "OFFSET_INVALID"], "photos.uploadProfilePhoto": ["FILE_PART_0_MISSING", "FILE_PARTS_INVALID", "IMAGE_PROCESS_FAILED", "PHOTO_CROP_SIZE_SMALL", "PHOTO_EXT_INVALID"], "upload.saveBigFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "FILE_PART_SIZE_INVALID", "FILE_PARTS_INVALID"], "upload.saveFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID"], "auth.signUp": ["FIRSTNAME_INVALID", "PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_OCCUPIED"], "messages.saveGif": ["GIF_ID_INVALID"], "account.resetAuthorization": ["HASH_INVALID"], "account.sendConfirmPhoneCode": ["HASH_INVALID"], "messages.sendInlineBotResult": ["INLINE_RESULT_EXPIRED", "PEER_ID_INVALID", "QUERY_ID_EMPTY"], "messages.getDialogs": ["INPUT_CONSTRUCTOR_INVALID", "OFFSET_PEER_ID_INVALID"], "messages.search": ["INPUT_CONSTRUCTOR_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "PEER_ID_NOT_SUPPORTED", "SEARCH_QUERY_EMPTY", "USER_ID_INVALID"], "messages.checkChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID"], "messages.importChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID", "USER_ALREADY_PARTICIPANT", "USERS_TOO_MUCH"], "{}": ["INVITE_HASH_EXPIRED"], "langpack.getDifference": ["LANG_PACK_INVALID"], "langpack.getLangPack": ["LANG_PACK_INVALID"], "langpack.getLanguages": ["LANG_PACK_INVALID"], "langpack.getStrings": ["LANG_PACK_INVALID"], "upload.getWebFile": ["LOCATION_INVALID"], "photos.getUserPhotos": ["MAX_ID_INVALID", "USER_ID_INVALID"], "auth.sendInvites": ["MESSAGE_EMPTY"], "messages.editInlineBotMessage": ["MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED"], "messages.getInlineGameHighScores": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.setInlineGameScore": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "payments.getPaymentForm": ["MESSAGE_ID_INVALID"], "payments.getPaymentReceipt": ["MESSAGE_ID_INVALID"], "payments.sendPaymentForm": ["MESSAGE_ID_INVALID"], "payments.validateRequestedInfo": ["MESSAGE_ID_INVALID"], "messages.readEncryptedHistory": ["MSG_WAIT_FAILED"], "messages.receivedQueue": ["MSG_WAIT_FAILED"], "messages.sendEncryptedFile": ["MSG_WAIT_FAILED"], "account.updatePasswordSettings": ["NEW_SALT_INVALID", "NEW_SETTINGS_INVALID", "PASSWORD_HASH_INVALID"], "auth.requestPasswordRecovery": ["PASSWORD_EMPTY"], "account.getPasswordSettings": ["PASSWORD_HASH_INVALID"], "account.getTmpPassword": ["PASSWORD_HASH_INVALID", "TMP_PASSWORD_DISABLED"], "auth.checkPassword": ["PASSWORD_HASH_INVALID"], "account.getNotifySettings": ["PEER_ID_INVALID"], "account.reportPeer": ["PEER_ID_INVALID"], "account.updateNotifySettings": ["PEER_ID_INVALID"], "contacts.resetTopPeerRating": ["PEER_ID_INVALID"], "messages.deleteHistory": ["PEER_ID_INVALID"], "messages.getGameHighScores": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getMessageEditData": ["PEER_ID_INVALID"], "messages.getUnreadMentions": ["PEER_ID_INVALID"], "messages.hideReportSpam": ["PEER_ID_INVALID"], "messages.readHistory": ["PEER_ID_INVALID"], "messages.reorderPinnedDialogs": ["PEER_ID_INVALID"], "messages.reportSpam": ["PEER_ID_INVALID"], "messages.saveDraft": ["PEER_ID_INVALID"], "messages.sendScreenshotNotification": ["PEER_ID_INVALID"], "messages.setGameScore": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.toggleDialogPin": ["PEER_ID_INVALID"], "auth.signIn": ["PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_UNOCCUPIED"], "auth.checkPhone": ["PHONE_NUMBER_BANNED", "PHONE_NUMBER_INVALID"], "account.changePhone": ["PHONE_NUMBER_INVALID"], "account.sendChangePhoneCode": ["PHONE_NUMBER_INVALID"], "auth.cancelCode": ["PHONE_NUMBER_INVALID"], "auth.resendCode": ["PHONE_NUMBER_INVALID"], "account.getPrivacy": ["PRIVACY_KEY_INVALID"], "account.setPrivacy": ["PRIVACY_KEY_INVALID"], "bots.answerWebhookJSONQuery": ["QUERY_ID_INVALID", "USER_BOT_INVALID"], "messages.setBotCallbackAnswer": ["QUERY_ID_INVALID"], "messages.setBotShippingResults": ["QUERY_ID_INVALID"], "contacts.search": ["QUERY_TOO_SHORT", "SEARCH_QUERY_EMPTY"], "messages.getDhConfig": ["RANDOM_LENGTH_INVALID"], "upload.reuploadCdnFile": ["RSA_DECRYPT_FAILED"], "messages.searchGifs": ["SEARCH_QUERY_EMPTY"], "messages.searchGlobal": ["SEARCH_QUERY_EMPTY"], "messages.getDocumentByHash": ["SHA256_HASH_INVALID"], "messages.faveSticker": ["STICKER_ID_INVALID"], "messages.saveRecentSticker": ["STICKER_ID_INVALID"], "messages.getStickerSet": ["STICKERSET_INVALID"], "messages.installStickerSet": ["STICKERSET_INVALID"], "messages.uninstallStickerSet": ["STICKERSET_INVALID"], "account.registerDevice": ["TOKEN_INVALID"], "account.unregisterDevice": ["TOKEN_INVALID"], "account.setAccountTTL": ["TTL_DAYS_INVALID"], "contacts.getTopPeers": ["TYPES_EMPTY"], "bots.sendCustomRequest": ["USER_BOT_INVALID"], "messages.getCommonChats": ["USER_ID_INVALID"], "users.getFullUser": ["USER_ID_INVALID"], "account.checkUsername": ["USERNAME_INVALID"], "account.updateUsername": ["USERNAME_INVALID", "USERNAME_NOT_MODIFIED", "USERNAME_OCCUPIED"], "contacts.resolveUsername": ["USERNAME_INVALID", "USERNAME_NOT_OCCUPIED"], "messages.createChat": ["USERS_TOO_FEW"]}, "401": {"contacts.resolveUsername": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "messages.getHistory": ["AUTH_KEY_PERM_EMPTY"], "auth.signIn": ["SESSION_PASSWORD_NEEDED"], "messages.getDialogs": ["SESSION_PASSWORD_NEEDED"], "updates.getDifference": ["SESSION_PASSWORD_NEEDED"], "updates.getState": ["SESSION_PASSWORD_NEEDED"], "upload.saveFilePart": ["SESSION_PASSWORD_NEEDED"], "users.getUsers": ["SESSION_PASSWORD_NEEDED"]}, "500": {"auth.sendCode": ["AUTH_RESTART"], "phone.acceptCall": ["CALL_OCCUPY_FAILED"], "messages.acceptEncryption": ["ENCRYPTION_OCCUPY_FAILED"], "updates.getChannelDifference": ["HISTORY_GET_FAILED", "PERSISTENT_TIMESTAMP_OUTDATED"], "users.getUsers": ["MEMBER_NO_LOCATION", "NEED_MEMBER_INVALID"], "auth.signUp": ["MEMBER_OCCUPY_PRIMARY_LOC_FAILED", "REG_ID_GENERATE_FAILED"], "channels.getChannels": ["NEED_CHAT_INVALID"], "messages.editChatTitle": ["NEED_CHAT_INVALID"], "contacts.deleteContacts": ["NEED_MEMBER_INVALID"], "contacts.importCard": ["NEED_MEMBER_INVALID"], "updates.getDifference": ["NEED_MEMBER_INVALID"], "phone.requestCall": ["PARTICIPANT_CALL_FAILED"], "messages.forwardMessages": ["PTS_CHANGE_EMPTY", "RANDOM_ID_DUPLICATE"], "messages.sendMessage": ["RANDOM_ID_DUPLICATE"], "messages.sendMedia": ["STORAGE_CHECK_FAILED"], "upload.getCdnFile": ["UNKNOWN_METHOD"]}, "403": {"channels.getFullChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "updates.getChannelDifference": ["CHANNEL_PUBLIC_GROUP_NA"], "channels.editAdmin": ["CHAT_ADMIN_INVITE_REQUIRED", "RIGHT_FORBIDDEN", "USER_PRIVACY_RESTRICTED"], "messages.migrateChat": ["CHAT_ADMIN_REQUIRED"], "messages.forwardMessages": ["CHAT_SEND_GIFS_FORBIDDEN", "CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_SEND_STICKERS_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "channels.inviteToChannel": ["CHAT_WRITE_FORBIDDEN", "USER_CHANNELS_TOO_MUCH", "USER_PRIVACY_RESTRICTED"], "messages.editMessage": ["CHAT_WRITE_FORBIDDEN", "MESSAGE_AUTHOR_REQUIRED"], "messages.sendMedia": ["CHAT_WRITE_FORBIDDEN"], "messages.sendMessage": ["CHAT_WRITE_FORBIDDEN"], "messages.setTyping": ["CHAT_WRITE_FORBIDDEN"], "messages.getMessageEditData": ["MESSAGE_AUTHOR_REQUIRED"], "channels.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.setInlineBotResults": ["USER_BOT_INVALID"], "phone.requestCall": ["USER_IS_BLOCKED", "USER_PRIVACY_RESTRICTED"], "messages.addChatUser": ["USER_NOT_MUTUAL_CONTACT", "USER_PRIVACY_RESTRICTED"], "channels.createChannel": ["USER_RESTRICTED"], "messages.createChat": ["USER_RESTRICTED"]}, "406": {"auth.checkPhone": ["PHONE_NUMBER_INVALID"], "auth.sendCode": ["PHONE_PASSWORD_FLOOD"]}, "-503": {"auth.resetAuthorizations": ["Timeout"], "contacts.deleteContacts": ["Timeout"], "messages.forwardMessages": ["Timeout"], "messages.getBotCallbackAnswer": ["Timeout"], "messages.getHistory": ["Timeout"], "messages.getInlineBotResults": ["Timeout"], "updates.getState": ["Timeout"]}}, "human_result": {"-429": ["Too many requests"], "ABOUT_TOO_LONG": ["The provided bio is too long"], "ACCESS_TOKEN_EXPIRED": ["Bot token expired"], "ACCESS_TOKEN_INVALID": ["The provided token is not valid"], "ACTIVE_USER_REQUIRED": ["The method is only available to already activated users"], "API_ID_INVALID": ["The api_id/api_hash combination is invalid"], "ARTICLE_TITLE_EMPTY": ["The title of the article is empty"], "AUTH_BYTES_INVALID": ["The provided authorization is invalid"], "AUTH_KEY_PERM_EMPTY": ["The temporary auth key must be binded to the permanent auth key to use these methods."], "AUTH_KEY_UNREGISTERED": ["The authorization key has expired"], "AUTH_RESTART": ["Restart the authorization process"], "BOT_GROUPS_BLOCKED": ["This bot can't be added to groups"], "BOT_INLINE_DISABLED": ["This bot can't be used in inline mode"], "BOT_INVALID": ["This is not a valid bot"], "BOT_METHOD_INVALID": ["This method cannot be run by a bot"], "BOT_MISSING": ["This method can only be run by a bot"], "BOTS_TOO_MUCH": ["There are too many bots in this chat/channel"], "BUTTON_DATA_INVALID": ["The provided button data is invalid"], "BUTTON_TYPE_INVALID": ["The type of one of the buttons you provided is invalid"], "BUTTON_URL_INVALID": ["Button URL invalid"], "CALL_ALREADY_ACCEPTED": ["The call was already accepted"], "CALL_ALREADY_DECLINED": ["The call was already declined"], "CALL_OCCUPY_FAILED": ["The call failed because the user is already making another call"], "CALL_PEER_INVALID": ["The provided call peer object is invalid"], "CALL_PROTOCOL_FLAGS_INVALID": ["Call protocol flags invalid"], "CDN_METHOD_INVALID": ["You can't call this method in a CDN DC"], "CHANNEL_INVALID": ["The provided channel is invalid"], "CHANNEL_PRIVATE": ["You haven't joined this channel/supergroup"], "CHANNEL_PUBLIC_GROUP_NA": ["channel/supergroup not available"], "CHANNELS_TOO_MUCH": ["You have joined too many channels/supergroups"], "CHAT_ABOUT_NOT_MODIFIED": ["About text has not changed"], "CHAT_ADMIN_INVITE_REQUIRED": ["You do not have the rights to do this"], "CHAT_ADMIN_REQUIRED": ["You must be an admin in this chat to do this"], "CHAT_FORBIDDEN": ["You cannot write in this chat"], "CHAT_ID_EMPTY": ["The provided chat ID is empty"], "CHAT_ID_INVALID": ["The provided chat id is invalid"], "CHAT_NOT_MODIFIED": ["The pinned message wasn't modified"], "CHAT_SEND_GIFS_FORBIDDEN": ["You can't send gifs in this chat"], "CHAT_SEND_MEDIA_FORBIDDEN": ["You can't send media in this chat"], "CHAT_SEND_STICKERS_FORBIDDEN": ["You can't send stickers in this chat."], "CHAT_TITLE_EMPTY": ["No chat title provided"], "CHAT_WRITE_FORBIDDEN": ["You can't write in this chat"], "CODE_EMPTY": ["The provided code is empty"], "CODE_HASH_INVALID": ["Code hash invalid"], "CONNECTION_API_ID_INVALID": ["The provided API id is invalid"], "CONNECTION_LANG_PACK_INVALID": ["Language pack invalid"], "CONNECTION_LAYER_INVALID": ["Layer invalid"], "CONTACT_ID_INVALID": ["The provided contact ID is invalid"], "DATA_INVALID": ["Encrypted data invalid"], "DATA_JSON_INVALID": ["The provided JSON data is invalid"], "DATE_EMPTY": ["Date empty"], "DC_ID_INVALID": ["The provided DC ID is invalid"], "DH_G_A_INVALID": ["g_a invalid"], "ENCRYPTED_MESSAGE_INVALID": ["Encrypted message invalid"], "ENCRYPTION_ALREADY_ACCEPTED": ["Secret chat already accepted"], "ENCRYPTION_ALREADY_DECLINED": ["The secret chat was already declined"], "ENCRYPTION_ID_INVALID": ["The provided secret chat ID is invalid"], "ENCRYPTION_OCCUPY_FAILED": ["Internal server error while accepting secret chat"], "ENTITY_MENTION_USER_INVALID": ["You can't use this entity"], "ERROR_TEXT_EMPTY": ["The provided error message is empty"], "EXPORT_CARD_INVALID": ["Provided card is invalid"], "FIELD_NAME_EMPTY": ["The field with the name FIELD_NAME is missing"], "FIELD_NAME_INVALID": ["The field with the name FIELD_NAME is invalid"], "FILE_ID_INVALID": ["The provided file id is invalid"], "FILE_PART_0_MISSING": ["File part 0 missing"], "FILE_PART_EMPTY": ["The provided file part is empty"], "FILE_PART_INVALID": ["The file part number is invalid"], "FILE_PART_LENGTH_INVALID": ["The length of a file part is invalid"], "FILE_PART_SIZE_INVALID": ["The provided file part size is invalid"], "FILE_PARTS_INVALID": ["The number of file parts is invalid"], "FIRSTNAME_INVALID": ["The first name is invalid"], "FLOOD_WAIT_666": ["Spooky af m8"], "GIF_ID_INVALID": ["The provided GIF ID is invalid"], "HASH_INVALID": ["The provided hash is invalid"], "HISTORY_GET_FAILED": ["Fetching of history failed"], "IMAGE_PROCESS_FAILED": ["Failure while processing image"], "INLINE_RESULT_EXPIRED": ["The inline query expired"], "INPUT_CONSTRUCTOR_INVALID": ["The provided constructor is invalid"], "INPUT_FETCH_ERROR": ["An error occurred while deserializing TL parameters"], "INPUT_FETCH_FAIL": ["Failed deserializing TL payload"], "INPUT_LAYER_INVALID": ["The provided layer is invalid"], "INPUT_METHOD_INVALID": ["The provided method is invalid"], "INPUT_REQUEST_TOO_LONG": ["The request is too big"], "INPUT_USER_DEACTIVATED": ["The specified user was deleted"], "INTERDC_1_CALL_ERROR": ["An error occurred while communicating with DC 1"], "INTERDC_1_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 1"], "INTERDC_2_CALL_ERROR": ["An error occurred while communicating with DC 2"], "INTERDC_2_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 2"], "INTERDC_3_CALL_ERROR": ["An error occurred while communicating with DC 3"], "INTERDC_3_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 3"], "INTERDC_4_CALL_ERROR": ["An error occurred while communicating with DC 4"], "INTERDC_4_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 4"], "INTERDC_5_CALL_ERROR": ["An error occurred while communicating with DC 5"], "INTERDC_5_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 5"], "INVITE_HASH_EMPTY": ["The invite hash is empty"], "INVITE_HASH_EXPIRED": ["The invite link has expired"], "INVITE_HASH_INVALID": ["The invite hash is invalid"], "LANG_PACK_INVALID": ["The provided language pack is invalid"], "LASTNAME_INVALID": ["The last name is invalid"], "LIMIT_INVALID": ["The provided limit is invalid"], "LOCATION_INVALID": ["The provided location is invalid"], "MAX_ID_INVALID": ["The provided max ID is invalid"], "MD5_CHECKSUM_INVALID": ["The MD5 checksums do not match"], "MEDIA_CAPTION_TOO_LONG": ["The caption is too long"], "MEDIA_EMPTY": ["The provided media object is invalid"], "MEMBER_OCCUPY_PRIMARY_LOC_FAILED": ["Occupation of primary member location failed"], "MESSAGE_AUTHOR_REQUIRED": ["Message author required"], "MESSAGE_DELETE_FORBIDDEN": ["You can't delete one of the messages you tried to delete, most likely because it is a service message."], "MESSAGE_EDIT_TIME_EXPIRED": ["You can't edit this message anymore, too much time has passed since its creation."], "MESSAGE_EMPTY": ["The provided message is empty"], "MESSAGE_ID_INVALID": ["The provided message id is invalid"], "MESSAGE_IDS_EMPTY": ["No message ids were provided"], "MESSAGE_NOT_MODIFIED": ["The message text has not changed"], "MESSAGE_TOO_LONG": ["The provided message is too long"], "MSG_WAIT_FAILED": ["A waiting call returned an error"], "NEED_CHAT_INVALID": ["The provided chat is invalid"], "NEED_MEMBER_INVALID": ["The provided member is invalid"], "NEW_SALT_INVALID": ["The new salt is invalid"], "NEW_SETTINGS_INVALID": ["The new settings are invalid"], "OFFSET_INVALID": ["The provided offset is invalid"], "OFFSET_PEER_ID_INVALID": ["The provided offset peer is invalid"], "PACK_SHORT_NAME_INVALID": ["Short pack name invalid"], "PARTICIPANT_CALL_FAILED": ["Failure while making call"], "PARTICIPANT_VERSION_OUTDATED": ["The other participant does not use an up to date telegram client with support for calls"], "PARTICIPANTS_TOO_FEW": ["Not enough participants"], "PASSWORD_EMPTY": ["The provided password is empty"], "PASSWORD_HASH_INVALID": ["The provided password hash is invalid"], "PEER_FLOOD": ["Too many requests"], "PEER_ID_INVALID": ["The provided peer id is invalid"], "PEER_ID_NOT_SUPPORTED": ["The provided peer ID is not supported"], "PERSISTENT_TIMESTAMP_EMPTY": ["Persistent timestamp empty"], "PERSISTENT_TIMESTAMP_INVALID": ["Persistent timestamp invalid"], "PERSISTENT_TIMESTAMP_OUTDATED": ["Persistent timestamp outdated"], "PHONE_CODE_EMPTY": ["phone_code is missing"], "PHONE_CODE_EXPIRED": ["The phone code you provided has expired, this may happen if it was sent to any chat on telegram (if the code is sent through a telegram chat (not the official account) to avoid it append or prepend to the code some chars)"], "PHONE_CODE_HASH_EMPTY": ["phone_code_hash is missing"], "PHONE_CODE_INVALID": ["The provided phone code is invalid"], "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN": [""], "PHONE_NUMBER_BANNED": ["The provided phone number is banned from telegram"], "PHONE_NUMBER_FLOOD": ["You asked for the code too many times."], "PHONE_NUMBER_INVALID": ["The phone number is invalid"], "PHONE_NUMBER_OCCUPIED": ["The phone number is already in use"], "PHONE_NUMBER_UNOCCUPIED": ["The phone number is not yet being used"], "PHONE_PASSWORD_FLOOD": ["You have tried logging in too many times"], "PHONE_PASSWORD_PROTECTED": ["This phone is password protected"], "PHOTO_CROP_SIZE_SMALL": ["Photo is too small"], "PHOTO_EXT_INVALID": ["The extension of the photo is invalid"], "PHOTO_INVALID_DIMENSIONS": ["The photo dimensions are invalid"], "PRIVACY_KEY_INVALID": ["The privacy key is invalid"], "PTS_CHANGE_EMPTY": ["No PTS change"], "QUERY_ID_EMPTY": ["The query ID is empty"], "QUERY_ID_INVALID": ["The query ID is invalid"], "QUERY_TOO_SHORT": ["The query string is too short"], "RANDOM_ID_DUPLICATE": ["You provided a random ID that was already used"], "RANDOM_ID_INVALID": ["A provided random ID is invalid"], "RANDOM_LENGTH_INVALID": ["Random length invalid"], "RANGES_INVALID": ["Invalid range provided"], "REG_ID_GENERATE_FAILED": ["Failure while generating registration ID"], "REPLY_MARKUP_INVALID": ["The provided reply markup is invalid"], "RESULT_TYPE_INVALID": ["Result type invalid"], "RIGHT_FORBIDDEN": ["Your admin rights do not allow you to do this"], "RPC_CALL_FAIL": ["Telegram is having internal issues, please try again later."], "RPC_MCGET_FAIL": ["Telegram is having internal issues, please try again later."], "RSA_DECRYPT_FAILED": ["Internal RSA decryption failed"], "SEARCH_QUERY_EMPTY": ["The search query is empty"], "SEND_MESSAGE_TYPE_INVALID": ["The message type is invalid"], "SESSION_PASSWORD_NEEDED": ["2FA is enabled, use a password to login"], "SHA256_HASH_INVALID": ["The provided SHA256 hash is invalid"], "START_PARAM_EMPTY": ["The start parameter is empty"], "START_PARAM_INVALID": ["Start parameter invalid"], "STICKER_ID_INVALID": ["The provided sticker ID is invalid"], "STICKER_INVALID": ["The provided sticker is invalid"], "STICKERS_EMPTY": ["No sticker provided"], "STICKERSET_INVALID": ["The provided sticker set is invalid"], "STORAGE_CHECK_FAILED": ["Server storage check failed"], "TEMP_AUTH_KEY_EMPTY": ["No temporary auth key provided"], "Timeout": ["A timeout occurred while fetching data from the bot"], "TMP_PASSWORD_DISABLED": ["The temporary password is disabled"], "TOKEN_INVALID": ["The provided token is invalid"], "TTL_DAYS_INVALID": ["The provided TTL is invalid"], "TYPE_CONSTRUCTOR_INVALID": ["The type constructor is invalid"], "TYPES_EMPTY": ["The types field is empty"], "UNKNOWN_METHOD": ["The method you tried to call cannot be called on non-CDN DCs"], "USER_ADMIN_INVALID": ["You're not an admin"], "USER_ALREADY_PARTICIPANT": ["The user is already in the group"], "USER_BANNED_IN_CHANNEL": ["You're banned from sending messages in supergroups/channels"], "USER_BLOCKED": ["User blocked"], "USER_BOT": ["Bots can only be admins in channels."], "USER_BOT_INVALID": ["This method can only be called by a bot"], "USER_BOT_REQUIRED": ["This method can only be called by a bot"], "USER_CHANNELS_TOO_MUCH": ["One of the users you tried to add is already in too many channels/supergroups"], "USER_CREATOR": ["You can't leave this channel, because you're its creator"], "USER_DEACTIVATED": ["The user was deactivated"], "USER_ID_INVALID": ["The provided user ID is invalid"], "USER_IS_BLOCKED": ["User is blocked"], "USER_IS_BOT": ["Bots can't send messages to other bots"], "USER_KICKED": ["This user was kicked from this supergroup/channel"], "USER_NOT_MUTUAL_CONTACT": ["The provided user is not a mutual contact"], "USER_NOT_PARTICIPANT": ["You're not a member of this supergroup/channel"], "USER_PRIVACY_RESTRICTED": ["The user's privacy settings do not allow you to do this"], "USER_RESTRICTED": ["You're spamreported, you can't create channels or chats."], "USERNAME_INVALID": ["The provided username is not valid"], "USERNAME_NOT_MODIFIED": ["The username was not modified"], "USERNAME_NOT_OCCUPIED": ["The provided username is not occupied"], "USERNAME_OCCUPIED": ["The provided username is already occupied"], "USERS_TOO_FEW": ["Not enough users (to create a chat, for example)"], "USERS_TOO_MUCH": ["The maximum number of users has been exceeded (to create a chat, for example)"], "WEBPAGE_CURL_FAILED": ["Failure while fetching the webpage with cURL"], "YOU_BLOCKED_USER": ["You blocked this user"]}} \ No newline at end of file From be9358282a06b456ef0112a64c2d11e4bd89abcc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 20 Oct 2017 17:29:45 +0200 Subject: [PATCH 52/58] Generate and fetch new errors from setup.py --- .gitignore | 2 +- setup.py | 18 +++++++++++++++--- telethon_generator/error_generator.py | 8 +++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index dd5de4c3..156d23e3 100755 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ telethon/tl/functions/ telethon/tl/types/ telethon/tl/all_tlobjects.py -telethon/tl/errors/rpc_error_list.py +telethon/errors/rpc_error_list.py # User session *.session diff --git a/setup.py b/setup.py index 695ad1a5..3ac7a90b 100755 --- a/setup.py +++ b/setup.py @@ -42,17 +42,25 @@ 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' +IMPORT_DEPTH = 2 + + def gen_tl(): from telethon_generator.tl_generator import TLGenerator + from telethon_generator.error_generator import generate_code generator = TLGenerator('telethon/tl') 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.') @@ -80,6 +88,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_generator/error_generator.py b/telethon_generator/error_generator.py index fb5d0ef7..81bcda5c 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -4,8 +4,6 @@ import urllib.request from collections import defaultdict URL = 'https://rpc.pwrtelegram.xyz/?all' -OUTPUT = '../telethon/errors/rpc_error_list.py' -JSON_OUTPUT = 'errors.json' known_base_classes = { 303: 'InvalidDCError', @@ -26,7 +24,7 @@ known_codes = { } -def fetch_errors(url=URL, output=JSON_OUTPUT): +def fetch_errors(output, url=URL): print('Opening a connection to', url, '...') r = urllib.request.urlopen(url) print('Checking response...') @@ -78,7 +76,7 @@ def write_error(f, code, name, desc, capture_name): f.write(')\n') -def generate_code(json_file=JSON_OUTPUT, output=OUTPUT): +def generate_code(output, json_file, errors_desc): with open(json_file, encoding='utf-8') as f: data = json.load(f) @@ -113,7 +111,7 @@ def generate_code(json_file=JSON_OUTPUT, output=OUTPUT): # Prefer the descriptions that are related with Telethon way of coding to # those that PWRTelegram's API provides. telethon_descriptions = {} - with open('error_descriptions', encoding='utf-8') as f: + with open(errors_desc, encoding='utf-8') as f: for line in f: line = line.strip() if line and not line.startswith('#'): From 83595a0e2d8d5d3f0e7ac642e2a8b8398845d353 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 20 Oct 2017 17:32:30 +0200 Subject: [PATCH 53/58] Use more constants in setup.py --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3ac7a90b..13be0144 100755 --- a/setup.py +++ b/setup.py @@ -46,13 +46,14 @@ 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 from telethon_generator.error_generator import generate_code - generator = TLGenerator('telethon/tl') + generator = TLGenerator(GENERATOR_DIR) if generator.tlobjects_exist(): print('Detected previous TLObjects. Cleaning...') generator.clean_tlobjects() @@ -71,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': From 033119e9b8f1b56f19b897f89d35c2b4d975556f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 20 Oct 2017 22:07:45 +0200 Subject: [PATCH 54/58] Make MtProtoSender._need_confirmation a set This will avoid adding duplicated items to it --- telethon/network/mtproto_sender.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 61e60484..8c73390f 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() @@ -183,7 +183,7 @@ class MtProtoSender: ) return False - self._need_confirmation.append(msg_id) + self._need_confirmation.add(msg_id) code = reader.read_int(signed=False) reader.seek(-4) From 1a91c024fcc29235a1f1e03ee755b076dc2dec4f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 20 Oct 2017 22:12:03 +0200 Subject: [PATCH 55/58] Revert 63dfb1e as many updates were being dropped --- telethon/network/mtproto_sender.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 8c73390f..3f98c5f3 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -174,15 +174,6 @@ class MtProtoSender: """ # TODO Check salt, session_id and sequence_number - if msg_id in self._need_confirmation: - # We're yet to acknowledge this message already, so just drop it - # as we are already aware of it. TODO Should we force acknowledging - # all the self._need_confirmation IDs? - self._logger.debug( - 'Ignoring message pending of acknowledge: {}'.format(msg_id) - ) - return False - self._need_confirmation.add(msg_id) code = reader.read_int(signed=False) From 2782a08ed03f0bfbf148284d176e0e4ca3c84b9e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 20 Oct 2017 22:44:00 +0200 Subject: [PATCH 56/58] Add note for future self when handling gzip packed data --- telethon/network/mtproto_sender.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 3f98c5f3..50281e9b 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -412,6 +412,11 @@ class MtProtoSender: def _handle_gzip_packed(self, msg_id, sequence, reader, state): self._logger.debug('Handling gzip packed data') with BinaryReader(GzipPacked.read(reader)) as compressed_reader: + # We are reentering process_msg, which seemingly the same msg_id + # to the self._need_confirmation set. Remove it from there first + # to avoid any future conflicts (i.e. if we "ignore" messages + # that we are already aware of, see 1a91c02 and old 63dfb1e) + self._need_confirmation -= {msg_id} return self._process_msg(msg_id, sequence, compressed_reader, state) # endregion From d70811b693f7bd4f899fa655e68498012696b29d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 20 Oct 2017 23:30:02 +0200 Subject: [PATCH 57/58] Fix infinite loop when invoking on update handlers (fix #336) Every update that hadn't been acknowledged on the main connection yet would be resent on any new connection. These new connections are made temporary when invoking anything from any thread that's not the main thread. It would also process all the updates, hence, Telegram would be resending these not-acknowledged updates to the temporary connection, and the updates would be processed again, then the update handler would react to the duplicated updates over and over. To fix this, simply don't process updates on the temporary thread at all. With this reasoning, if we don't acknowledge updates on the temporary connections, Telegram will resend them on the main connection, so we should not lose any. --- telethon/telegram_bare_client.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 0b31c4c5..2a5ce4f5 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -432,9 +432,18 @@ class TelegramBareClient: on_main_thread = threading.get_ident() == self._main_thread_ident if on_main_thread or self._on_read_thread(): sender = self._sender + update_state = self.updates else: sender = self._sender.clone() sender.connect() + # We're on another connection, Telegram will resend all the + # updates that we haven't acknowledged (potentially entering + # an infinite loop if we're calling this in response to an + # update event, as it would be received again and again). So + # to avoid this we will simply not process updates on these + # new temporary connections, as they will be sent and later + # acknowledged over the main connection. + update_state = None # We should call receive from this thread if there's no background # thread reading or if the server disconnected us and we're trying @@ -447,7 +456,9 @@ class TelegramBareClient: if self._background_error and on_main_thread: raise self._background_error - result = self._invoke(sender, call_receive, *requests) + result = self._invoke( + sender, call_receive, update_state, *requests + ) if result is not None: return result @@ -459,7 +470,7 @@ class TelegramBareClient: # Let people use client.invoke(SomeRequest()) instead client(...) invoke = __call__ - def _invoke(self, sender, call_receive, *requests): + def _invoke(self, sender, call_receive, update_state, *requests): try: # Ensure that we start with no previous errors (i.e. resending) for x in requests: @@ -479,7 +490,7 @@ class TelegramBareClient: ) else: while not all(x.confirm_received.is_set() for x in requests): - sender.receive(update_state=self.updates) + sender.receive(update_state=update_state) except TimeoutError: pass # We will just retry @@ -526,7 +537,7 @@ class TelegramBareClient: # be on the very first connection (not authorized, not running), # but may be an issue for people who actually travel? self._reconnect(new_dc=e.new_dc) - return self._invoke(sender, call_receive, *requests) + return self._invoke(sender, call_receive, update_state, *requests) except ServerError as e: # Telegram is having some issues, just retry From 5cdf92e50960300c1cb8514afc1362fb67618e4a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 20 Oct 2017 23:33:08 +0200 Subject: [PATCH 58/58] Update to v0.15.3 --- telethon/telegram_bare_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 2a5ce4f5..53edb008 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -58,7 +58,7 @@ class TelegramBareClient: """ # Current TelegramClient version - __version__ = '0.15.2' + __version__ = '0.15.3' # TODO Make this thread-safe, all connections share the same DC _dc_options = None