From 91a5d20e93e54a5b2672acb6322a943863e20f5b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 7 Oct 2017 13:26:09 +0200 Subject: [PATCH 01/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] .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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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()