From c218df87d7fa0aa13ead7080fed84b5c9a3ac3ef Mon Sep 17 00:00:00 2001 From: Tanuj Date: Mon, 25 Dec 2017 16:26:29 +0000 Subject: [PATCH 001/361] Remove reference to README.rst (#504) --- 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 6c7d3ab0..6c258c9a 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -86,7 +86,7 @@ class TelegramBareClient: if not api_id or not api_hash: raise PermissionError( "Your API ID or Hash cannot be empty or None. " - "Refer to Telethon's README.rst for more information.") + "Refer to Telethon's wiki for more information.") self._use_ipv6 = use_ipv6 From b11c2e885bd8c5babeb83d15adb6ac6b6611cc99 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 25 Dec 2017 17:59:39 +0100 Subject: [PATCH 002/361] Fix assertion for multiple same flag parameters --- telethon_generator/tl_generator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index f8a9e873..4adb5378 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -311,8 +311,10 @@ class TLGenerator: for ra in repeated_args.values(): if len(ra) > 1: - cnd1 = ('self.{}'.format(a.name) for a in ra) - cnd2 = ('not self.{}'.format(a.name) for a in ra) + cnd1 = ('(self.{0} or self.{0} is not None)' + .format(a.name) for a in ra) + cnd2 = ('(self.{0} is None or self.{0} is False)' + .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( From 664417b40949decd44fc12e09ce4d296f93fab39 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 26 Dec 2017 16:45:47 +0100 Subject: [PATCH 003/361] Use sqlite3 instead JSON for the session files --- telethon/telegram_bare_client.py | 2 +- telethon/tl/session.py | 232 +++++++++++++++++++++---------- 2 files changed, 162 insertions(+), 72 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 6c258c9a..d4f19b8d 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -92,7 +92,7 @@ class TelegramBareClient: # Determine what session object we have if isinstance(session, str) or session is None: - session = Session.try_load_or_create_new(session) + session = Session(session) elif not isinstance(session, Session): raise ValueError( 'The given session must be a str or a Session instance.' diff --git a/telethon/tl/session.py b/telethon/tl/session.py index e530cc83..e9885a56 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -1,15 +1,19 @@ import json import os import platform +import sqlite3 import struct import time -from base64 import b64encode, b64decode +from base64 import b64decode from os.path import isfile as file_exists from threading import Lock from .entity_database import EntityDatabase from .. import helpers +EXTENSION = '.session' +CURRENT_VERSION = 1 # database version + class Session: """This session contains the required information to login into your @@ -25,6 +29,7 @@ class Session: those required to init a connection will be copied. """ # These values will NOT be saved + self.filename = ':memory:' if isinstance(session_user_id, Session): self.session_user_id = None @@ -41,7 +46,10 @@ class Session: self.flood_sleep_threshold = session.flood_sleep_threshold else: # str / None - self.session_user_id = session_user_id + if session_user_id: + self.filename = session_user_id + if not self.filename.endswith(EXTENSION): + self.filename += EXTENSION system = platform.uname() self.device_model = system.system if system.system else 'Unknown' @@ -54,49 +62,172 @@ class Session: self.save_entities = True self.flood_sleep_threshold = 60 + # These values will be saved + self._server_address = None + self._port = None + self._auth_key = None + self._layer = 0 + self._salt = 0 # Signed long + self.entities = EntityDatabase() # Known and cached entities + # Cross-thread safety self._seq_no_lock = Lock() self._msg_id_lock = Lock() - self._save_lock = Lock() + self._db_lock = Lock() + + # Migrating from .json -> SQL + self._check_migrate_json() + + self._conn = sqlite3.connect(self.filename, check_same_thread=False) + c = self._conn.cursor() + c.execute("select name from sqlite_master " + "where type='table' and name='version'") + if c.fetchone(): + # Tables already exist, check for the version + c.execute("select version from version") + version = c.fetchone()[0] + if version != CURRENT_VERSION: + self._upgrade_database(old=version) + self.save() + + # These values will be saved + c.execute('select * from sessions') + self._server_address, self._port, key, \ + self._layer, self._salt = c.fetchone() + + from ..crypto import AuthKey + self._auth_key = AuthKey(data=key) + c.close() + else: + # Tables don't exist, create new ones + c.execute("create table version (version integer)") + c.execute( + """create table sessions ( + server_address text, + port integer, + auth_key blob, + layer integer, + salt integer + )""" + ) + c.execute( + """create table entities ( + id integer, + hash integer, + username text, + phone integer, + name text + )""" + ) + c.execute("insert into version values (1)") + c.close() + self.save() self.id = helpers.generate_random_long(signed=True) self._sequence = 0 self.time_offset = 0 self._last_msg_id = 0 # Long - # These values will be saved - self.server_address = None - self.port = None - self.auth_key = None - self.layer = 0 - self.salt = 0 # Signed long - self.entities = EntityDatabase() # Known and cached entities + def _check_migrate_json(self): + if file_exists(self.filename): + try: + with open(self.filename, encoding='utf-8') as f: + data = json.load(f) + self._port = data.get('port', self._port) + self._salt = data.get('salt', self._salt) + # Keep while migrating from unsigned to signed salt + if self._salt > 0: + self._salt = struct.unpack( + 'q', struct.pack('Q', self._salt))[0] + + self._layer = data.get('layer', self._layer) + self._server_address = \ + data.get('server_address', self._server_address) + + from ..crypto import AuthKey + if data.get('auth_key_data', None) is not None: + key = b64decode(data['auth_key_data']) + self._auth_key = AuthKey(data=key) + + self.entities = EntityDatabase(data.get('entities', [])) + self.delete() # Delete JSON file to create database + except (UnicodeDecodeError, json.decoder.JSONDecodeError): + pass + + def _upgrade_database(self, old): + pass + + # Data from sessions should be kept as properties + # not to fetch the database every time we need it + @property + def server_address(self): + return self._server_address + + @server_address.setter + def server_address(self, value): + self._server_address = value + self._update_session_table() + + @property + def port(self): + return self._port + + @port.setter + def port(self, value): + self._port = value + self._update_session_table() + + @property + def auth_key(self): + return self._auth_key + + @auth_key.setter + def auth_key(self, value): + self._auth_key = value + self._update_session_table() + + @property + def layer(self): + return self._layer + + @layer.setter + def layer(self, value): + self._layer = value + self._update_session_table() + + @property + def salt(self): + return self._salt + + @salt.setter + def salt(self, value): + self._salt = value + self._update_session_table() + + def _update_session_table(self): + with self._db_lock: + c = self._conn.cursor() + c.execute('delete from sessions') + c.execute('insert into sessions values (?,?,?,?,?)', ( + self._server_address, + self._port, + self._auth_key.key if self._auth_key else b'', + self._layer, + self._salt + )) + c.close() def save(self): """Saves the current session object as session_user_id.session""" - if not self.session_user_id or self._save_lock.locked(): - return - - with self._save_lock: - with open('{}.session'.format(self.session_user_id), 'w') as file: - out_dict = { - 'port': self.port, - 'salt': self.salt, - 'layer': self.layer, - 'server_address': self.server_address, - 'auth_key_data': - b64encode(self.auth_key.key).decode('ascii') - if self.auth_key else None - } - if self.save_entities: - out_dict['entities'] = self.entities.get_input_list() - - json.dump(out_dict, file) + with self._db_lock: + self._conn.commit() def delete(self): """Deletes the current session file""" + if self.filename == ':memory:': + return True try: - os.remove('{}.session'.format(self.session_user_id)) + os.remove(self.filename) return True except OSError: return False @@ -107,48 +238,7 @@ class Session: using this client and never logged out """ return [os.path.splitext(os.path.basename(f))[0] - for f in os.listdir('.') if f.endswith('.session')] - - @staticmethod - def try_load_or_create_new(session_user_id): - """Loads a saved session_user_id.session or creates a new one. - If session_user_id=None, later .save()'s will have no effect. - """ - if session_user_id is None: - return Session(None) - else: - path = '{}.session'.format(session_user_id) - result = Session(session_user_id) - if not file_exists(path): - return result - - try: - with open(path, 'r') as file: - data = json.load(file) - result.port = data.get('port', result.port) - result.salt = data.get('salt', result.salt) - # Keep while migrating from unsigned to signed salt - if result.salt > 0: - result.salt = struct.unpack( - 'q', struct.pack('Q', result.salt))[0] - - result.layer = data.get('layer', result.layer) - result.server_address = \ - data.get('server_address', result.server_address) - - # FIXME We need to import the AuthKey here or otherwise - # we get cyclic dependencies. - from ..crypto import AuthKey - if data.get('auth_key_data', None) is not None: - key = b64decode(data['auth_key_data']) - result.auth_key = AuthKey(data=key) - - result.entities = EntityDatabase(data.get('entities', [])) - - except (json.decoder.JSONDecodeError, UnicodeDecodeError): - pass - - return result + for f in os.listdir('.') if f.endswith(EXTENSION)] def generate_sequence(self, content_related): """Thread safe method to generates the next sequence number, From 0a4849b150b284908cd75af9f43e08d3870f7a26 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 26 Dec 2017 16:59:30 +0100 Subject: [PATCH 004/361] Small cleanup of the Session class --- telethon/tl/session.py | 55 ++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index e9885a56..ff4631f8 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -23,37 +23,34 @@ class Session: If you think the session has been compromised, close all the sessions through an official Telegram client to revoke the authorization. """ - def __init__(self, session_user_id): + def __init__(self, session_id): """session_user_id should either be a string or another Session. Note that if another session is given, only parameters like those required to init a connection will be copied. """ # These values will NOT be saved self.filename = ':memory:' - if isinstance(session_user_id, Session): - self.session_user_id = None - - # For connection purposes - session = session_user_id - self.device_model = session.device_model - self.system_version = session.system_version - self.app_version = session.app_version - self.lang_code = session.lang_code - self.system_lang_code = session.system_lang_code - 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 + # For connection purposes + if isinstance(session_id, Session): + self.device_model = session_id.device_model + self.system_version = session_id.system_version + self.app_version = session_id.app_version + self.lang_code = session_id.lang_code + self.system_lang_code = session_id.system_lang_code + self.lang_pack = session_id.lang_pack + self.report_errors = session_id.report_errors + self.save_entities = session_id.save_entities + self.flood_sleep_threshold = session_id.flood_sleep_threshold else: # str / None - if session_user_id: - self.filename = session_user_id + if session_id: + self.filename = session_id if not self.filename.endswith(EXTENSION): self.filename += EXTENSION system = platform.uname() - self.device_model = system.system if system.system else 'Unknown' - self.system_version = system.release if system.release else '1.0' + self.device_model = system.system or 'Unknown' + self.system_version = system.release or '1.0' self.app_version = '1.0' # '0' will provoke error self.lang_code = 'en' self.system_lang_code = self.lang_code @@ -62,6 +59,16 @@ class Session: self.save_entities = True self.flood_sleep_threshold = 60 + self.id = helpers.generate_random_long(signed=True) + self._sequence = 0 + self.time_offset = 0 + self._last_msg_id = 0 # Long + + # Cross-thread safety + self._seq_no_lock = Lock() + self._msg_id_lock = Lock() + self._db_lock = Lock() + # These values will be saved self._server_address = None self._port = None @@ -70,11 +77,6 @@ class Session: self._salt = 0 # Signed long self.entities = EntityDatabase() # Known and cached entities - # Cross-thread safety - self._seq_no_lock = Lock() - self._msg_id_lock = Lock() - self._db_lock = Lock() - # Migrating from .json -> SQL self._check_migrate_json() @@ -123,11 +125,6 @@ class Session: c.close() self.save() - self.id = helpers.generate_random_long(signed=True) - self._sequence = 0 - self.time_offset = 0 - self._last_msg_id = 0 # Long - def _check_migrate_json(self): if file_exists(self.filename): try: From aef96f1b6898a5a4b48b3a6943eb574ab5df1052 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 00:50:09 +0100 Subject: [PATCH 005/361] Remove custom EntityDatabase and use sqlite3 instead There are still a few things to change, like cleaning up the code and actually caching the entities as a whole (currently, although the username/phone/name can be used to fetch their input version which is an improvement, their full version needs to be re-fetched. Maybe it's a good thing though?) --- telethon/telegram_client.py | 65 ++++----- telethon/tl/entity_database.py | 252 --------------------------------- telethon/tl/session.py | 137 ++++++++++++++++-- telethon/utils.py | 30 ++++ 4 files changed, 181 insertions(+), 303 deletions(-) delete mode 100644 telethon/tl/entity_database.py diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 32ade1a9..5d09ee2c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -19,7 +19,6 @@ from .errors import ( from .network import ConnectionMode from .tl import TLObject from .tl.custom import Draft, Dialog -from .tl.entity_database import EntityDatabase from .tl.functions.account import ( GetPasswordRequest ) @@ -144,7 +143,7 @@ class TelegramClient(TelegramBareClient): :return auth.SentCode: Information about the result of the request. """ - phone = EntityDatabase.parse_phone(phone) or self._phone + phone = utils.parse_phone(phone) or self._phone if not self._phone_code_hash: result = self(SendCodeRequest(phone, self.api_id, self.api_hash)) @@ -188,7 +187,7 @@ class TelegramClient(TelegramBareClient): if phone and not code: return self.send_code_request(phone) elif code: - phone = EntityDatabase.parse_phone(phone) or self._phone + phone = utils.parse_phone(phone) or self._phone phone_code_hash = phone_code_hash or self._phone_code_hash if not phone: raise ValueError( @@ -1009,12 +1008,8 @@ class TelegramClient(TelegramBareClient): may be out of date. :return: """ - if not force_fetch: - # Try to use cache unless we want to force a fetch - try: - return self.session.entities[entity] - except KeyError: - pass + # TODO Actually cache {id: entities} again + # >>> if not force_fetch: reuse cached if isinstance(entity, int) or ( isinstance(entity, TLObject) and @@ -1022,36 +1017,38 @@ class TelegramClient(TelegramBareClient): type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)): ie = self.get_input_entity(entity) if isinstance(ie, InputPeerUser): - self(GetUsersRequest([ie])) + return self(GetUsersRequest([ie]))[0] elif isinstance(ie, InputPeerChat): - self(GetChatsRequest([ie.chat_id])) + return self(GetChatsRequest([ie.chat_id])).chats[0] elif isinstance(ie, InputPeerChannel): - 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 + return self(GetChannelsRequest([ie])).chats[0] if isinstance(entity, str): - return self._get_entity_from_string(entity) + # TODO This probably can be done better... + invite = self._load_entity_from_string(entity) + if invite: + return invite + return self.get_entity(self.session.get_input_entity(entity)) raise ValueError( 'Cannot turn "{}" into any entity (user or chat)'.format(entity) ) - def _get_entity_from_string(self, string): - """Gets an entity from the given string, which may be a phone or - an username, and processes all the found entities on the session. + def _load_entity_from_string(self, string): """ - phone = EntityDatabase.parse_phone(string) + Loads an entity from the given string, which may be a phone or + an username, and processes all the found entities on the session. + + This method will effectively add the found users to the session + database, so it can be queried later. + + May return a channel or chat if the string was an invite. + """ + phone = utils.parse_phone(string) if phone: - entity = phone self(GetContactsRequest(0)) else: - entity, is_join_chat = EntityDatabase.parse_username(string) + entity, is_join_chat = utils.parse_username(string) if is_join_chat: invite = self(CheckChatInviteRequest(entity)) if isinstance(invite, ChatInvite): @@ -1063,13 +1060,6 @@ class TelegramClient(TelegramBareClient): return invite.chat else: self(ResolveUsernameRequest(entity)) - # MtProtoSender will call .process_entities on the requests made - try: - return self.session.entities[entity] - except KeyError: - raise ValueError( - 'Could not find user with username {}'.format(entity) - ) def get_input_entity(self, peer): """ @@ -1092,12 +1082,15 @@ class TelegramClient(TelegramBareClient): """ try: # First try to get the entity from cache, otherwise figure it out - return self.session.entities.get_input_entity(peer) + return self.session.get_input_entity(peer) except KeyError: pass if isinstance(peer, str): - return utils.get_input_peer(self._get_entity_from_string(peer)) + invite = self._load_entity_from_string(peer) + if invite: + return utils.get_input_peer(invite) + return self.session.get_input_entity(peer) is_peer = False if isinstance(peer, int): @@ -1130,7 +1123,7 @@ class TelegramClient(TelegramBareClient): exclude_pinned=True )) try: - return self.session.entities.get_input_entity(peer) + return self.session.get_input_entity(peer) except KeyError: pass diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py deleted file mode 100644 index 9002ebd8..00000000 --- a/telethon/tl/entity_database.py +++ /dev/null @@ -1,252 +0,0 @@ -import re -from threading import Lock - -from ..tl import TLObject -from ..tl.types import ( - User, Chat, Channel, PeerUser, PeerChat, PeerChannel, - InputPeerUser, InputPeerChat, InputPeerChannel -) -from .. import utils # Keep this line the last to maybe fix #357 - - -USERNAME_RE = re.compile( - r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' -) - - -class EntityDatabase: - def __init__(self, input_list=None, enabled=True, enabled_full=True): - """Creates a new entity database with an initial load of "Input" - entities, if any. - - If 'enabled', input entities will be saved. The whole entity - will be saved if both 'enabled' and 'enabled_full' are True. - """ - self.enabled = enabled - self.enabled_full = enabled_full - - self._lock = Lock() - self._entities = {} # marked_id: user|chat|channel - - if input_list: - # TODO For compatibility reasons some sessions were saved with - # 'access_hash': null in the JSON session file. Drop these, as - # it means we don't have access to such InputPeers. Issue #354. - self._input_entities = { - k: v for k, v in input_list if v is not None - } - else: - self._input_entities = {} # marked_id: hash - - # TODO Allow disabling some extra mappings - self._username_id = {} # username: marked_id - self._phone_id = {} # phone: marked_id - - def process(self, tlobject): - """Processes all the found entities on the given TLObject, - unless .enabled is False. - - Returns True if new input entities were added. - """ - if not self.enabled: - return False - - # Save all input entities we know of - if not isinstance(tlobject, TLObject) and hasattr(tlobject, '__iter__'): - # This may be a list of users already for instance - return self.expand(tlobject) - - entities = [] - if hasattr(tlobject, 'chats') and hasattr(tlobject.chats, '__iter__'): - entities.extend(tlobject.chats) - if hasattr(tlobject, 'users') and hasattr(tlobject.users, '__iter__'): - entities.extend(tlobject.users) - - return self.expand(entities) - - def expand(self, entities): - """Adds new input entities to the local database unconditionally. - Unknown types will be ignored. - """ - if not entities or not self.enabled: - return False - - new = [] # Array of entities (User, Chat, or Channel) - new_input = {} # Dictionary of {entity_marked_id: access_hash} - for e in entities: - if not isinstance(e, TLObject): - continue - - try: - p = utils.get_input_peer(e, allow_self=False) - marked_id = utils.get_peer_id(p, add_mark=True) - - has_hash = False - if isinstance(p, InputPeerChat): - # Chats don't have a hash - new_input[marked_id] = 0 - has_hash = True - elif p.access_hash: - # Some users and channels seem to be returned without - # an 'access_hash', meaning Telegram doesn't want you - # to access them. This is the reason behind ensuring - # that the 'access_hash' is non-zero. See issue #354. - new_input[marked_id] = p.access_hash - has_hash = True - - if self.enabled_full and has_hash: - if isinstance(e, (User, Chat, Channel)): - new.append(e) - except ValueError: - pass - - with self._lock: - before = len(self._input_entities) - self._input_entities.update(new_input) - for e in new: - self._add_full_entity(e) - return len(self._input_entities) != before - - def _add_full_entity(self, entity): - """Adds a "full" entity (User, Chat or Channel, not "Input*"), - despite the value of self.enabled and self.enabled_full. - - Not to be confused with UserFull, ChatFull, or ChannelFull, - "full" means simply not "Input*". - """ - marked_id = utils.get_peer_id( - utils.get_input_peer(entity, allow_self=False), add_mark=True - ) - try: - old_entity = self._entities[marked_id] - old_entity.__dict__.update(entity.__dict__) # Keep old references - - # Update must delete old username and phone - username = getattr(old_entity, 'username', None) - if username: - del self._username_id[username.lower()] - - phone = getattr(old_entity, 'phone', None) - if phone: - del self._phone_id[phone] - except KeyError: - # Add new entity - self._entities[marked_id] = entity - - # Always update username or phone if any - username = getattr(entity, 'username', None) - if username: - self._username_id[username.lower()] = marked_id - - phone = getattr(entity, 'phone', None) - if phone: - self._phone_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. - - 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) - try: - if phone: - return self._phone_id[phone] - else: - username, _ = EntityDatabase.parse_username(key) - return self._username_id[username.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) - - if callable(key): - for k, v in self._entities.items(): - if key(v): - return k - - raise ValueError() - - def __getitem__(self, key): - """See the ._parse_key() docstring for possible values of the key""" - try: - return self._entities[self._parse_key(key)] - except (ValueError, KeyError) as e: - raise KeyError(key) from e - - def __delitem__(self, key): - 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 - - 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): - """Parses the given phone, or returns None if it's invalid""" - if isinstance(phone, int): - return str(phone) - else: - phone = re.sub(r'[+()\s-]', '', str(phone)) - if phone.isdigit(): - return phone - - @staticmethod - def parse_username(username): - """Parses the given username or channel access hash, given - a string, username or URL. Returns a tuple consisting of - both the stripped username and whether it is a joinchat/ hash. - """ - username = username.strip() - m = USERNAME_RE.match(username) - if m: - return username[m.end():], bool(m.group(1)) - else: - return username, False - - def get_input_entity(self, peer): - try: - 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: - return InputPeerChat(i) - elif k == PeerChannel: - return InputPeerChannel(i, h) - - except ValueError as e: - raise KeyError(peer) from e - raise KeyError(peer) - - def get_input_list(self): - return list(self._input_entities.items()) - - def clear(self, target=None): - if target is None: - self._entities.clear() - else: - del self[target] diff --git a/telethon/tl/session.py b/telethon/tl/session.py index ff4631f8..12bc3937 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -8,8 +8,12 @@ from base64 import b64decode from os.path import isfile as file_exists from threading import Lock -from .entity_database import EntityDatabase -from .. import helpers +from .. import utils, helpers +from ..tl import TLObject +from ..tl.types import ( + PeerUser, PeerChat, PeerChannel, + InputPeerUser, InputPeerChat, InputPeerChannel +) EXTENSION = '.session' CURRENT_VERSION = 1 # database version @@ -75,10 +79,9 @@ class Session: self._auth_key = None self._layer = 0 self._salt = 0 # Signed long - self.entities = EntityDatabase() # Known and cached entities # Migrating from .json -> SQL - self._check_migrate_json() + entities = self._check_migrate_json() self._conn = sqlite3.connect(self.filename, check_same_thread=False) c = self._conn.cursor() @@ -114,14 +117,20 @@ class Session: ) c.execute( """create table entities ( - id integer, - hash integer, + id integer primary key, + hash integer not null, username text, phone integer, name text )""" ) c.execute("insert into version values (1)") + # Migrating from JSON -> new table and may have entities + if entities: + c.executemany( + 'insert or replace into entities values (?,?,?,?,?)', + entities + ) c.close() self.save() @@ -130,6 +139,8 @@ class Session: try: with open(self.filename, encoding='utf-8') as f: data = json.load(f) + self.delete() # Delete JSON file to create database + self._port = data.get('port', self._port) self._salt = data.get('salt', self._salt) # Keep while migrating from unsigned to signed salt @@ -146,10 +157,12 @@ class Session: key = b64decode(data['auth_key_data']) self._auth_key = AuthKey(data=key) - self.entities = EntityDatabase(data.get('entities', [])) - self.delete() # Delete JSON file to create database + rows = [] + for p_id, p_hash in data.get('entities', []): + rows.append((p_id, p_hash, None, None, None)) + return rows except (UnicodeDecodeError, json.decoder.JSONDecodeError): - pass + return [] # No entities def _upgrade_database(self, old): pass @@ -275,9 +288,103 @@ class Session: correct = correct_msg_id >> 32 self.time_offset = correct - now - def process_entities(self, tlobject): - try: - if self.entities.process(tlobject): - self.save() # Save if any new entities got added - except: - pass + # Entity processing + + def process_entities(self, tlo): + """Processes all the found entities on the given TLObject, + unless .enabled is False. + + Returns True if new input entities were added. + """ + if not self.save_entities: + return + + if not isinstance(tlo, TLObject) and hasattr(tlo, '__iter__'): + # This may be a list of users already for instance + entities = tlo + else: + entities = [] + if hasattr(tlo, 'chats') and hasattr(tlo.chats, '__iter__'): + entities.extend(tlo.chats) + if hasattr(tlo, 'users') and hasattr(tlo.users, '__iter__'): + entities.extend(tlo.users) + if not entities: + return + + rows = [] # Rows to add (id, hash, username, phone, name) + for e in entities: + if not isinstance(e, TLObject): + continue + try: + p = utils.get_input_peer(e, allow_self=False) + marked_id = utils.get_peer_id(p, add_mark=True) + + p_hash = None + if isinstance(p, InputPeerChat): + p_hash = 0 + elif p.access_hash: + # Some users and channels seem to be returned without + # an 'access_hash', meaning Telegram doesn't want you + # to access them. This is the reason behind ensuring + # that the 'access_hash' is non-zero. See issue #354. + p_hash = p.access_hash + + if p_hash is not None: + username = getattr(e, 'username', None) + phone = getattr(e, 'phone', None) + name = utils.get_display_name(e) or None + rows.append((marked_id, p_hash, username, phone, name)) + except ValueError: + pass + if not rows: + return + + with self._db_lock: + self._conn.executemany( + 'insert or replace into entities values (?,?,?,?,?)', rows + ) + self.save() + + def get_input_entity(self, key): + """Parses the given string, integer or TLObject key into a + marked entity ID, which is then used to fetch the hash + from the database. + + If a callable key is given, every row will be fetched, + and passed as a tuple to a function, that should return + a true-like value when the desired row is found. + + Raises ValueError if it cannot be found. + """ + c = self._conn.cursor() + if isinstance(key, str): + phone = utils.parse_phone(key) + if phone: + c.execute('select id, hash from entities where phone=?', + (phone,)) + else: + username, _ = utils.parse_username(key) + c.execute('select id, hash from entities where username=?', + (username,)) + + if isinstance(key, TLObject): + # crc32(b'InputPeer') and crc32(b'Peer') + if type(key).SUBCLASS_OF_ID == 0xc91c90b6: + return key + key = utils.get_peer_id(key, add_mark=True) + + if isinstance(key, int): + c.execute('select id, hash from entities where id=?', (key,)) + + result = c.fetchone() + if result: + i, h = result # unpack resulting tuple + i, k = utils.resolve_id(i) # removes the mark and returns kind + if k == PeerUser: + return InputPeerUser(i, h) + elif k == PeerChat: + return InputPeerChat(i) + elif k == PeerChannel: + return InputPeerChannel(i, h) + else: + raise ValueError('Could not find input entity with key ', key) diff --git a/telethon/utils.py b/telethon/utils.py index 5e92b13d..04970632 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -5,6 +5,8 @@ to convert between an entity like an User, Chat, etc. into its Input version) import math from mimetypes import add_type, guess_extension +import re + from .tl import TLObject from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, @@ -24,6 +26,11 @@ from .tl.types import ( ) +USERNAME_RE = re.compile( + r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' +) + + def get_display_name(entity): """Gets the input peer for the given "entity" (user, chat or channel) Returns None if it was not found""" @@ -305,6 +312,29 @@ def get_input_media(media, user_caption=None, is_photo=False): _raise_cast_fail(media, 'InputMedia') +def parse_phone(phone): + """Parses the given phone, or returns None if it's invalid""" + if isinstance(phone, int): + return str(phone) + else: + phone = re.sub(r'[+()\s-]', '', str(phone)) + if phone.isdigit(): + return phone + + +def parse_username(username): + """Parses the given username or channel access hash, given + a string, username or URL. Returns a tuple consisting of + both the stripped username and whether it is a joinchat/ hash. + """ + username = username.strip() + m = USERNAME_RE.match(username) + if m: + return username[m.end():], bool(m.group(1)) + else: + return username, 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. From 86429e7291a79cea8bdda27fa1f5b64860b1ba69 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 11:54:08 +0100 Subject: [PATCH 006/361] Lowercase usernames before adding them to the database --- telethon/tl/session.py | 13 ++++++------- telethon/utils.py | 9 ++++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 12bc3937..e3dea190 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -330,7 +330,7 @@ class Session: p_hash = p.access_hash if p_hash is not None: - username = getattr(e, 'username', None) + username = getattr(e, 'username', '').lower() or None phone = getattr(e, 'phone', None) name = utils.get_display_name(e) or None rows.append((marked_id, p_hash, username, phone, name)) @@ -357,6 +357,11 @@ class Session: Raises ValueError if it cannot be found. """ c = self._conn.cursor() + if isinstance(key, TLObject): + if type(key).SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') + return key + key = utils.get_peer_id(key, add_mark=True) + if isinstance(key, str): phone = utils.parse_phone(key) if phone: @@ -367,12 +372,6 @@ class Session: c.execute('select id, hash from entities where username=?', (username,)) - if isinstance(key, TLObject): - # crc32(b'InputPeer') and crc32(b'Peer') - if type(key).SUBCLASS_OF_ID == 0xc91c90b6: - return key - key = utils.get_peer_id(key, add_mark=True) - if isinstance(key, int): c.execute('select id, hash from entities where id=?', (key,)) diff --git a/telethon/utils.py b/telethon/utils.py index 04970632..0662a99d 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -325,14 +325,17 @@ def parse_phone(phone): def parse_username(username): """Parses the given username or channel access hash, given a string, username or URL. Returns a tuple consisting of - both the stripped username and whether it is a joinchat/ hash. + both the stripped, lowercase username and whether it is + a joinchat/ hash (in which case is not lowercase'd). """ username = username.strip() m = USERNAME_RE.match(username) if m: - return username[m.end():], bool(m.group(1)) + result = username[m.end():] + is_invite = bool(m.group(1)) + return result if is_invite else result.lower(), is_invite else: - return username, False + return username.lower(), False def get_peer_id(peer, add_mark=False): From 5c17097d8d011b0af11bebe0d1cedfad4c25ab7c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 11:56:05 +0100 Subject: [PATCH 007/361] Clean up .get_entity and remove force_fetch --- telethon/telegram_client.py | 83 +++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5d09ee2c..c1eab9fa 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -982,7 +982,7 @@ class TelegramClient(TelegramBareClient): # region Small utilities to make users' life easier - def get_entity(self, entity, force_fetch=False): + def get_entity(self, entity): """ Turns the given entity into a valid Telegram user or chat. @@ -1001,16 +1001,8 @@ class TelegramClient(TelegramBareClient): If the entity is neither, and it's not a TLObject, an error will be raised. - :param force_fetch: - If True, the entity cache is bypassed and the entity is fetched - again with an API call. Defaults to False to avoid unnecessary - calls, but since a cached version would be returned, the entity - may be out of date. - :return: + :return: User, Chat or Channel corresponding to the input entity. """ - # TODO Actually cache {id: entities} again - # >>> if not force_fetch: reuse cached - if isinstance(entity, int) or ( isinstance(entity, TLObject) and # crc32(b'InputPeer') and crc32(b'Peer') @@ -1024,33 +1016,33 @@ class TelegramClient(TelegramBareClient): return self(GetChannelsRequest([ie])).chats[0] if isinstance(entity, str): - # TODO This probably can be done better... - invite = self._load_entity_from_string(entity) - if invite: - return invite - return self.get_entity(self.session.get_input_entity(entity)) + return self._get_entity_from_string(entity) raise ValueError( 'Cannot turn "{}" into any entity (user or chat)'.format(entity) ) - def _load_entity_from_string(self, string): + def _get_entity_from_string(self, string): """ - Loads an entity from the given string, which may be a phone or + Gets a full entity from the given string, which may be a phone or an username, and processes all the found entities on the session. + The string may also be a user link, or a channel/chat invite link. - This method will effectively add the found users to the session - database, so it can be queried later. + This method has the side effect of adding the found users to the + session database, so it can be queried later without API calls, + if this option is enabled on the session. - May return a channel or chat if the string was an invite. + Returns the found entity. """ phone = utils.parse_phone(string) if phone: - self(GetContactsRequest(0)) + for user in self(GetContactsRequest(0)).users: + if user.phone == phone: + return user else: - entity, is_join_chat = utils.parse_username(string) + string, is_join_chat = utils.parse_username(string) if is_join_chat: - invite = self(CheckChatInviteRequest(entity)) + invite = self(CheckChatInviteRequest(string)) if isinstance(invite, ChatInvite): # If it's an invite to a chat, the user must join before # for the link to be resolved and work, otherwise raise. @@ -1059,7 +1051,10 @@ class TelegramClient(TelegramBareClient): elif isinstance(invite, ChatInviteAlready): return invite.chat else: - self(ResolveUsernameRequest(entity)) + result = self(ResolveUsernameRequest(string)) + for entity in itertools.chain(result.users, result.chats): + if entity.username.lower() == string: + return entity def get_input_entity(self, peer): """ @@ -1078,7 +1073,8 @@ class TelegramClient(TelegramBareClient): If in the end the access hash required for the peer was not found, a ValueError will be raised. - :return: + + :return: InputPeerUser, InputPeerChat or InputPeerChannel. """ try: # First try to get the entity from cache, otherwise figure it out @@ -1087,10 +1083,7 @@ class TelegramClient(TelegramBareClient): pass if isinstance(peer, str): - invite = self._load_entity_from_string(peer) - if invite: - return utils.get_input_peer(invite) - return self.session.get_input_entity(peer) + return utils.get_input_peer(self._get_entity_from_string(peer)) is_peer = False if isinstance(peer, int): @@ -1110,22 +1103,22 @@ class TelegramClient(TelegramBareClient): 'Cannot turn "{}" into an input entity.'.format(peer) ) - if self.session.save_entities: - # 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.get_input_entity(peer) - except KeyError: - pass + # 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. + dialogs = self(GetDialogsRequest( + offset_date=None, + offset_id=0, + offset_peer=InputPeerEmpty(), + limit=0, + exclude_pinned=True + )) + + target = utils.get_peer_id(peer, add_mark=True) + for entity in itertools.chain(dialogs.users, dialogs.chats): + if utils.get_peer_id(entity, add_mark=True) == target: + return utils.get_input_peer(entity) raise ValueError( 'Could not find the input entity corresponding to "{}".' From b6b47d175c73d9461a7952414f98f2a4e99566d7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 12:16:49 +0100 Subject: [PATCH 008/361] Fix username.lower() on instances with username field but None --- telethon/tl/session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index e3dea190..8fcbf31d 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -330,7 +330,9 @@ class Session: p_hash = p.access_hash if p_hash is not None: - username = getattr(e, 'username', '').lower() or None + username = getattr(e, 'username', None) or None + if username is not None: + username = username.lower() phone = getattr(e, 'phone', None) name = utils.get_display_name(e) or None rows.append((marked_id, p_hash, username, phone, name)) From 3512028d0ffaaaf2cbb4850a73f42e2b69a3f7ee Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 12:36:14 +0100 Subject: [PATCH 009/361] Fix .get_input_entity excepting wrong type --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c1eab9fa..3a264b42 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1079,7 +1079,7 @@ class TelegramClient(TelegramBareClient): try: # First try to get the entity from cache, otherwise figure it out return self.session.get_input_entity(peer) - except KeyError: + except ValueError: pass if isinstance(peer, str): From f96d88d3b5e6527efa0e9b3dd7e4b98abdcd40a1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 12:36:38 +0100 Subject: [PATCH 010/361] Modify .get_entity to support fetching many entities at once --- telethon/telegram_client.py | 56 ++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3a264b42..5aa08c42 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -987,7 +987,7 @@ class TelegramClient(TelegramBareClient): Turns the given entity into a valid Telegram user or chat. :param entity: - The entity to be transformed. + The entity (or iterable of entities) to be transformed. If it's a string which can be converted to an integer or starts with '+' it will be resolved as if it were a phone number. @@ -1003,24 +1003,46 @@ class TelegramClient(TelegramBareClient): :return: User, Chat or Channel corresponding to the input entity. """ - if isinstance(entity, int) or ( - isinstance(entity, TLObject) and - # crc32(b'InputPeer') and crc32(b'Peer') - type(entity).SUBCLASS_OF_ID in (0xc91c90b6, 0x2d45687)): - ie = self.get_input_entity(entity) - if isinstance(ie, InputPeerUser): - return self(GetUsersRequest([ie]))[0] - elif isinstance(ie, InputPeerChat): - return self(GetChatsRequest([ie.chat_id])).chats[0] - elif isinstance(ie, InputPeerChannel): - return self(GetChannelsRequest([ie])).chats[0] + if not isinstance(entity, str) and hasattr(entity, '__iter__'): + single = False + else: + single = True + entity = (entity,) - if isinstance(entity, str): - return self._get_entity_from_string(entity) + # Group input entities by string (resolve username), + # input users (get users), input chat (get chats) and + # input channels (get channels) to get the most entities + # in the less amount of calls possible. + inputs = [ + x if isinstance(x, str) else self.get_input_entity(x) + for x in entity + ] + users = [x for x in inputs if isinstance(x, InputPeerUser)] + chats = [x.chat_id for x in inputs if isinstance(x, InputPeerChat)] + channels = [x for x in inputs if isinstance(x, InputPeerChannel)] + if users: + users = self(GetUsersRequest(users)) + if chats: # TODO Handle chats slice? + chats = self(GetChatsRequest(chats)).chats + if channels: + channels = self(GetChannelsRequest(channels)).chats - raise ValueError( - 'Cannot turn "{}" into any entity (user or chat)'.format(entity) - ) + # Merge users, chats and channels into a single dictionary + id_entity = { + utils.get_peer_id(x, add_mark=True): x + for x in itertools.chain(users, chats, channels) + } + + # We could check saved usernames and put them into the users, + # chats and channels list from before. While this would reduce + # the amount of ResolveUsername calls, it would fail to catch + # username changes. + result = [ + self._get_entity_from_string(x) if isinstance(x, str) + else id_entity[utils.get_peer_id(x, add_mark=True)] + for x in inputs + ] + return result[0] if single else result def _get_entity_from_string(self, string): """ From f8745155ac5440c2e1b1dfb0a206ba9e2ac20d13 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 12:37:07 +0100 Subject: [PATCH 011/361] Stop joining read thread on disconnect, as it may be None --- telethon/telegram_bare_client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 6c258c9a..233dfdb7 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -260,10 +260,6 @@ class TelegramBareClient: __log__.debug('Disconnecting the socket...') self._sender.disconnect() - if self._recv_thread: - __log__.debug('Joining the read thread...') - self._recv_thread.join() - # TODO Shall we clear the _exported_sessions, or may be reused? pass From 843e777eba04946cc649c091bc762908597edfe8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 12:58:50 +0100 Subject: [PATCH 012/361] Simplify .process_entities() flow --- telethon/tl/session.py | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 8fcbf31d..c19b37db 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -318,26 +318,23 @@ class Session: try: p = utils.get_input_peer(e, allow_self=False) marked_id = utils.get_peer_id(p, add_mark=True) - - p_hash = None - if isinstance(p, InputPeerChat): - p_hash = 0 - elif p.access_hash: - # Some users and channels seem to be returned without - # an 'access_hash', meaning Telegram doesn't want you - # to access them. This is the reason behind ensuring - # that the 'access_hash' is non-zero. See issue #354. - p_hash = p.access_hash - - if p_hash is not None: - username = getattr(e, 'username', None) or None - if username is not None: - username = username.lower() - phone = getattr(e, 'phone', None) - name = utils.get_display_name(e) or None - rows.append((marked_id, p_hash, username, phone, name)) except ValueError: - pass + continue + + p_hash = getattr(p, 'access_hash', 0) + if p_hash is None: + # Some users and channels seem to be returned without + # an 'access_hash', meaning Telegram doesn't want you + # to access them. This is the reason behind ensuring + # that the 'access_hash' is non-zero. See issue #354. + continue + + username = getattr(e, 'username', None) or None + if username is not None: + username = username.lower() + phone = getattr(e, 'phone', None) + name = utils.get_display_name(e) or None + rows.append((marked_id, p_hash, username, phone, name)) if not rows: return From 932ed9ea9d7ccd0ec833984295405a5f028ad6cd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 13:06:03 +0100 Subject: [PATCH 013/361] Cast to input peer early on get input entity and close cursor --- telethon/tl/session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index c19b37db..1e374a54 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -355,12 +355,13 @@ class Session: Raises ValueError if it cannot be found. """ - c = self._conn.cursor() if isinstance(key, TLObject): + key = utils.get_input_peer(key) if type(key).SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') return key key = utils.get_peer_id(key, add_mark=True) + c = self._conn.cursor() if isinstance(key, str): phone = utils.parse_phone(key) if phone: @@ -375,6 +376,7 @@ class Session: c.execute('select id, hash from entities where id=?', (key,)) result = c.fetchone() + c.close() if result: i, h = result # unpack resulting tuple i, k = utils.resolve_id(i) # removes the mark and returns kind From f29ee41f6c3c914929f2e46289fecee01446d5ee Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 13:27:54 +0100 Subject: [PATCH 014/361] Don't use rowid for the entities table --- telethon/tl/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 1e374a54..3dfba1d9 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -122,7 +122,7 @@ class Session: username text, phone integer, name text - )""" + ) without rowid""" ) c.execute("insert into version values (1)") # Migrating from JSON -> new table and may have entities From 73edb0f4ff53a9e91f64481ae0d4e529064abebe Mon Sep 17 00:00:00 2001 From: Birger Jarl Date: Wed, 27 Dec 2017 16:52:33 +0300 Subject: [PATCH 015/361] Avoid using None dates on file download (#462) --- telethon/telegram_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 32ade1a9..3a0b9e4f 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -954,6 +954,8 @@ class TelegramClient(TelegramBareClient): name = None if not name: + if not date: + date = datetime.now() name = '{}_{}-{:02}-{:02}_{:02}-{:02}-{:02}'.format( kind, date.year, date.month, date.day, From 21e5f0b547703867cae9bac41ac42f94ba857911 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 15:08:29 +0100 Subject: [PATCH 016/361] Fix GetUsersRequest has a limit of 200 --- telethon/telegram_client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5aa08c42..67180cb3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1021,7 +1021,12 @@ class TelegramClient(TelegramBareClient): chats = [x.chat_id for x in inputs if isinstance(x, InputPeerChat)] channels = [x for x in inputs if isinstance(x, InputPeerChannel)] if users: - users = self(GetUsersRequest(users)) + # GetUsersRequest has a limit of 200 per call + tmp = [] + while users: + curr, users = users[:200], users[200:] + tmp.extend(self(GetUsersRequest(curr))) + users = tmp if chats: # TODO Handle chats slice? chats = self(GetChatsRequest(chats)).chats if channels: From f3d47769df830e91203e66cc76f6004e2e75ed62 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 15:26:23 +0100 Subject: [PATCH 017/361] Fix .send_read_acknowledge() for channels (#501) --- telethon/telegram_client.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3a0b9e4f..792ccd06 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -558,13 +558,13 @@ class TelegramClient(TelegramBareClient): return total_messages, messages, senders - def send_read_acknowledge(self, entity, messages=None, max_id=None): + def send_read_acknowledge(self, entity, message=None, max_id=None): """ Sends a "read acknowledge" (i.e., notifying the given peer that we've read their messages, also known as the "double check"). :param entity: The chat where these messages are located. - :param messages: Either a list of messages or a single message. + :param message: Either a list of messages or a single message. :param max_id: Overrides messages, until which message should the acknowledge should be sent. :return: @@ -574,15 +574,16 @@ class TelegramClient(TelegramBareClient): raise InvalidParameterError( 'Either a message list or a max_id must be provided.') - if isinstance(messages, list): - max_id = max(msg.id for msg in messages) + if hasattr(message, '__iter__'): + max_id = max(msg.id for msg in message) else: - max_id = messages.id + max_id = message.id - return self(ReadHistoryRequest( - peer=self.get_input_entity(entity), - max_id=max_id - )) + entity = self.get_input_entity(entity) + if entity == InputPeerChannel: + return self(channels.ReadHistoryRequest(entity, max_id=max_id)) + else: + return self(messages.ReadHistoryRequest(entity, max_id=max_id)) @staticmethod def _get_reply_to(reply_to): From a5b1457eee8ee33eff9e868ce4672892b5fd86a0 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Thu, 28 Dec 2017 07:33:25 +1000 Subject: [PATCH 018/361] TelegramBareClient: Fix lost #region --- 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 233dfdb7..27acfe9a 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -815,7 +815,7 @@ class TelegramBareClient: # endregion - # Constant read + # region Constant read def _set_connected_and_authorized(self): self._authorized = True From fa64a5f7b8a59745b6363a8fc7c4c9beb5edc6f7 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Thu, 28 Dec 2017 07:50:49 +1000 Subject: [PATCH 019/361] TelegramBareClient: Add set_proxy() method This allows to change proxy without recreation of the client instance. --- telethon/telegram_bare_client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 27acfe9a..8dad6d29 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -299,6 +299,13 @@ class TelegramBareClient: self.disconnect() return self.connect() + def set_proxy(proxy): + """Change the proxy used by the connections. + """ + if self.is_connected(): + raise RuntimeError("You can't change the proxy while connected.") + self._sender.connection.conn.proxy = proxy + # endregion # region Working with different connections/Data Centers From 292e4fc29f188b356ca266de5c36823b91d24d4c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 23:45:48 +0100 Subject: [PATCH 020/361] Fix .get_dialogs() being inconsistent with the return type --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 792ccd06..d8000b3b 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -301,7 +301,7 @@ class TelegramClient(TelegramBareClient): """ limit = float('inf') if limit is None else int(limit) if limit == 0: - return [], [] + return [] dialogs = OrderedDict() # Use peer id as identifier to avoid dupes while len(dialogs) < limit: From bdd63b91a21eb5a832b455f8bf30624beb22a721 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 23:54:31 +0100 Subject: [PATCH 021/361] Fix .download_profile_photo() for some channels (closes #500) --- telethon/telegram_client.py | 50 +++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index d8000b3b..e0708bc9 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -14,7 +14,8 @@ from . import TelegramBareClient from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, InvalidParameterError, PhoneCodeEmptyError, - PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError + PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, + LocationInvalidError ) from .network import ConnectionMode from .tl import TLObject @@ -43,7 +44,7 @@ from .tl.functions.users import ( GetUsersRequest ) from .tl.functions.channels import ( - GetChannelsRequest + GetChannelsRequest, GetFullChannelRequest ) from .tl.types import ( DocumentAttributeAudio, DocumentAttributeFilename, @@ -744,6 +745,7 @@ class TelegramClient(TelegramBareClient): None if no photo was provided, or if it was Empty. On success the file path is returned since it may differ from the one given. """ + photo = entity possible_names = [] if not isinstance(entity, TLObject) or type(entity).SUBCLASS_OF_ID in ( 0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697 @@ -769,31 +771,41 @@ class TelegramClient(TelegramBareClient): for attr in ('username', 'first_name', 'title'): possible_names.append(getattr(entity, attr, None)) - entity = entity.photo + photo = entity.photo - if not isinstance(entity, UserProfilePhoto) and \ - not isinstance(entity, ChatPhoto): + if not isinstance(photo, UserProfilePhoto) and \ + not isinstance(photo, ChatPhoto): return None - if download_big: - photo_location = entity.photo_big - else: - photo_location = entity.photo_small - + photo_location = photo.photo_big if download_big else photo.photo_small file = self._get_proper_filename( file, 'profile_photo', '.jpg', possible_names=possible_names ) # Download the media with the largest size input file location - self.download_file( - InputFileLocation( - volume_id=photo_location.volume_id, - local_id=photo_location.local_id, - secret=photo_location.secret - ), - file - ) + try: + self.download_file( + InputFileLocation( + volume_id=photo_location.volume_id, + local_id=photo_location.local_id, + secret=photo_location.secret + ), + file + ) + except LocationInvalidError: + # See issue #500, Android app fails as of v4.6.0 (1155). + # The fix seems to be using the full channel chat photo. + ie = self.get_input_entity(entity) + if isinstance(ie, InputPeerChannel): + full = self(GetFullChannelRequest(ie)) + return self._download_photo( + full.full_chat.chat_photo, file, + date=None, progress_callback=None + ) + else: + # Until there's a report for chats, no need to. + return None return file def download_media(self, message, file=None, progress_callback=None): @@ -833,7 +845,7 @@ class TelegramClient(TelegramBareClient): """Specialized version of .download_media() for photos""" # Determine the photo and its largest size - photo = mm_photo.photo + photo = getattr(mm_photo, 'photo', mm_photo) largest_size = photo.sizes[-1] file_size = largest_size.size largest_size = largest_size.location From b1b3610c1ff3ff6f852d37337befcb58afa8dda6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 00:09:29 +0100 Subject: [PATCH 022/361] Add missing self to .set_proxy (fa64a5f) --- 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 8dad6d29..f22d13e6 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -299,7 +299,7 @@ class TelegramBareClient: self.disconnect() return self.connect() - def set_proxy(proxy): + def set_proxy(self, proxy): """Change the proxy used by the connections. """ if self.is_connected(): From b252468ca293b1a72fc7af4184533b556e84df18 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Thu, 28 Dec 2017 07:50:49 +1000 Subject: [PATCH 023/361] TelegramBareClient: Add set_proxy() method This allows to change proxy without recreation of the client instance. --- telethon/telegram_bare_client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 27acfe9a..f22d13e6 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -299,6 +299,13 @@ class TelegramBareClient: self.disconnect() return self.connect() + def set_proxy(self, proxy): + """Change the proxy used by the connections. + """ + if self.is_connected(): + raise RuntimeError("You can't change the proxy while connected.") + self._sender.connection.conn.proxy = proxy + # endregion # region Working with different connections/Data Centers From 166d5a401237ee56eb6e14a26dea8cb66648bb3f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 23:45:48 +0100 Subject: [PATCH 024/361] Fix .get_dialogs() being inconsistent with the return type --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 792ccd06..d8000b3b 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -301,7 +301,7 @@ class TelegramClient(TelegramBareClient): """ limit = float('inf') if limit is None else int(limit) if limit == 0: - return [], [] + return [] dialogs = OrderedDict() # Use peer id as identifier to avoid dupes while len(dialogs) < limit: From 1a746e14643a91ae33d186a383d1cfdc36433081 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 27 Dec 2017 23:54:31 +0100 Subject: [PATCH 025/361] Fix .download_profile_photo() for some channels (closes #500) --- telethon/telegram_client.py | 50 +++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index d8000b3b..e0708bc9 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -14,7 +14,8 @@ from . import TelegramBareClient from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, InvalidParameterError, PhoneCodeEmptyError, - PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError + PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, + LocationInvalidError ) from .network import ConnectionMode from .tl import TLObject @@ -43,7 +44,7 @@ from .tl.functions.users import ( GetUsersRequest ) from .tl.functions.channels import ( - GetChannelsRequest + GetChannelsRequest, GetFullChannelRequest ) from .tl.types import ( DocumentAttributeAudio, DocumentAttributeFilename, @@ -744,6 +745,7 @@ class TelegramClient(TelegramBareClient): None if no photo was provided, or if it was Empty. On success the file path is returned since it may differ from the one given. """ + photo = entity possible_names = [] if not isinstance(entity, TLObject) or type(entity).SUBCLASS_OF_ID in ( 0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697 @@ -769,31 +771,41 @@ class TelegramClient(TelegramBareClient): for attr in ('username', 'first_name', 'title'): possible_names.append(getattr(entity, attr, None)) - entity = entity.photo + photo = entity.photo - if not isinstance(entity, UserProfilePhoto) and \ - not isinstance(entity, ChatPhoto): + if not isinstance(photo, UserProfilePhoto) and \ + not isinstance(photo, ChatPhoto): return None - if download_big: - photo_location = entity.photo_big - else: - photo_location = entity.photo_small - + photo_location = photo.photo_big if download_big else photo.photo_small file = self._get_proper_filename( file, 'profile_photo', '.jpg', possible_names=possible_names ) # Download the media with the largest size input file location - self.download_file( - InputFileLocation( - volume_id=photo_location.volume_id, - local_id=photo_location.local_id, - secret=photo_location.secret - ), - file - ) + try: + self.download_file( + InputFileLocation( + volume_id=photo_location.volume_id, + local_id=photo_location.local_id, + secret=photo_location.secret + ), + file + ) + except LocationInvalidError: + # See issue #500, Android app fails as of v4.6.0 (1155). + # The fix seems to be using the full channel chat photo. + ie = self.get_input_entity(entity) + if isinstance(ie, InputPeerChannel): + full = self(GetFullChannelRequest(ie)) + return self._download_photo( + full.full_chat.chat_photo, file, + date=None, progress_callback=None + ) + else: + # Until there's a report for chats, no need to. + return None return file def download_media(self, message, file=None, progress_callback=None): @@ -833,7 +845,7 @@ class TelegramClient(TelegramBareClient): """Specialized version of .download_media() for photos""" # Determine the photo and its largest size - photo = mm_photo.photo + photo = getattr(mm_photo, 'photo', mm_photo) largest_size = photo.sizes[-1] file_size = largest_size.size largest_size = largest_size.location From 6ec6967ff9a2e09aae70b500273075bdfbae975c Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Thu, 28 Dec 2017 09:22:28 +1000 Subject: [PATCH 026/361] Make exception types correspond to Python docs --- docs/docs_writer.py | 2 +- telethon/errors/__init__.py | 5 ++--- telethon/errors/common.py | 7 ------- telethon/extensions/binary_reader.py | 9 ++++----- telethon/extensions/tcp_client.py | 2 +- telethon/telegram_bare_client.py | 16 ++++++++-------- telethon/telegram_client.py | 15 +++++++-------- telethon/tl/custom/draft.py | 2 +- telethon/tl/tlobject.py | 3 ++- telethon/utils.py | 6 +++--- 10 files changed, 29 insertions(+), 38 deletions(-) diff --git a/docs/docs_writer.py b/docs/docs_writer.py index f9042f00..9eec6cd7 100644 --- a/docs/docs_writer.py +++ b/docs/docs_writer.py @@ -90,7 +90,7 @@ class DocsWriter: def end_menu(self): """Ends an opened menu""" if not self.menu_began: - raise ValueError('No menu had been started in the first place.') + raise RuntimeError('No menu had been started in the first place.') self.write('') def write_title(self, title, level=1): diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index fbb2f424..9126aca3 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -7,9 +7,8 @@ import re from threading import Thread from .common import ( - ReadCancelledError, InvalidParameterError, TypeNotFoundError, - InvalidChecksumError, BrokenAuthKeyError, SecurityError, - CdnFileTamperedError + ReadCancelledError, TypeNotFoundError, InvalidChecksumError, + BrokenAuthKeyError, SecurityError, CdnFileTamperedError ) # This imports the base errors too, as they're imported there diff --git a/telethon/errors/common.py b/telethon/errors/common.py index f2f21840..46b0b52e 100644 --- a/telethon/errors/common.py +++ b/telethon/errors/common.py @@ -7,13 +7,6 @@ class ReadCancelledError(Exception): super().__init__(self, 'The read operation was cancelled.') -class InvalidParameterError(Exception): - """ - Occurs when an invalid parameter is given, for example, - when either A or B are required but none is given. - """ - - class TypeNotFoundError(Exception): """ Occurs when a type is not found, for example, diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 19fb608b..460bed96 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -6,7 +6,7 @@ from datetime import datetime from io import BufferedReader, BytesIO from struct import unpack -from ..errors import InvalidParameterError, TypeNotFoundError +from ..errors import TypeNotFoundError from ..tl.all_tlobjects import tlobjects @@ -22,8 +22,7 @@ class BinaryReader: elif stream: self.stream = stream else: - raise InvalidParameterError( - 'Either bytes or a stream must be provided') + raise ValueError('Either bytes or a stream must be provided') self.reader = BufferedReader(self.stream) self._last = None # Should come in handy to spot -404 errors @@ -110,7 +109,7 @@ class BinaryReader: elif value == 0xbc799737: # boolFalse return False else: - raise ValueError('Invalid boolean code {}'.format(hex(value))) + raise RuntimeError('Invalid boolean code {}'.format(hex(value))) def tgread_date(self): """Reads and converts Unix time (used by Telegram) @@ -141,7 +140,7 @@ class BinaryReader: def tgread_vector(self): """Reads a vector (a list) of Telegram objects.""" if 0x1cb5c415 != self.read_int(signed=False): - raise ValueError('Invalid constructor code, vector was expected') + raise RuntimeError('Invalid constructor code, vector was expected') count = self.read_int() return [self.tgread_object() for _ in range(count)] diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index f59bb9f0..61be30f5 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -26,7 +26,7 @@ class TcpClient: elif isinstance(timeout, (int, float)): self.timeout = float(timeout) else: - raise ValueError('Invalid timeout type', type(timeout)) + raise TypeError('Invalid timeout type: {}'.format(type(timeout))) def _recreate_socket(self, mode): if self.proxy is None: diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index f22d13e6..36820629 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -84,7 +84,7 @@ class TelegramBareClient: **kwargs): """Refer to TelegramClient.__init__ for docs on this method""" if not api_id or not api_hash: - raise PermissionError( + raise ValueError( "Your API ID or Hash cannot be empty or None. " "Refer to Telethon's wiki for more information.") @@ -94,7 +94,7 @@ class TelegramBareClient: if isinstance(session, str) or session is None: session = Session.try_load_or_create_new(session) elif not isinstance(session, Session): - raise ValueError( + raise TypeError( 'The given session must be a str or a Session instance.' ) @@ -421,11 +421,11 @@ class TelegramBareClient: """Invokes (sends) a MTProtoRequest and returns (receives) its result. The invoke will be retried up to 'retries' times before raising - ValueError(). + RuntimeError(). """ if not all(isinstance(x, TLObject) and x.content_related for x in requests): - raise ValueError('You can only invoke requests, not types!') + raise TypeError('You can only invoke requests, not types!') # For logging purposes if len(requests) == 1: @@ -486,7 +486,7 @@ class TelegramBareClient: else: sender.connect() - raise ValueError('Number of retries reached 0.') + raise RuntimeError('Number of retries reached 0.') finally: if sender != self._sender: sender.disconnect() # Close temporary connections @@ -682,8 +682,8 @@ class TelegramBareClient: if progress_callback: progress_callback(stream.tell(), file_size) else: - raise ValueError('Failed to upload file part {}.' - .format(part_index)) + raise RuntimeError( + 'Failed to upload file part {}.'.format(part_index)) finally: stream.close() @@ -853,7 +853,7 @@ class TelegramBareClient: :return: """ if self._spawn_read_thread and not self._on_read_thread(): - raise ValueError('Can only idle if spawn_read_thread=False') + raise RuntimeError('Can only idle if spawn_read_thread=False') for sig in stop_signals: signal(sig, self._signal_handler) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e0708bc9..11d677ae 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -13,9 +13,8 @@ except ImportError: from . import TelegramBareClient from . import helpers, utils from .errors import ( - RPCError, UnauthorizedError, InvalidParameterError, PhoneCodeEmptyError, - PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, - LocationInvalidError + RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, + PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError ) from .network import ConnectionMode from .tl import TLObject @@ -381,7 +380,7 @@ class TelegramClient(TelegramBareClient): if parse_mode in {'md', 'markdown'}: message, msg_entities = markdown.parse(message) else: - raise ValueError('Unknown parsing mode', parse_mode) + raise ValueError('Unknown parsing mode: {}'.format(parse_mode)) else: msg_entities = [] @@ -572,7 +571,7 @@ class TelegramClient(TelegramBareClient): """ if max_id is None: if not messages: - raise InvalidParameterError( + raise ValueError( 'Either a message list or a max_id must be provided.') if hasattr(message, '__iter__'): @@ -600,7 +599,7 @@ class TelegramClient(TelegramBareClient): # hex(crc32(b'Message')) = 0x790009e3 return reply_to.id - raise ValueError('Invalid reply_to type: ', type(reply_to)) + raise TypeError('Invalid reply_to type: {}'.format(type(reply_to))) # endregion @@ -1053,7 +1052,7 @@ class TelegramClient(TelegramBareClient): if isinstance(entity, str): return self._get_entity_from_string(entity) - raise ValueError( + raise TypeError( 'Cannot turn "{}" into any entity (user or chat)'.format(entity) ) @@ -1128,7 +1127,7 @@ class TelegramClient(TelegramBareClient): pass if not is_peer: - raise ValueError( + raise TypeError( 'Cannot turn "{}" into an input entity.'.format(peer) ) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index c50baa78..abf84548 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -21,7 +21,7 @@ class Draft: @classmethod def _from_update(cls, client, update): if not isinstance(update, UpdateDraftMessage): - raise ValueError( + raise TypeError( 'You can only create a new `Draft` from a corresponding ' '`UpdateDraftMessage` object.' ) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index e2b23018..489765e2 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -97,7 +97,8 @@ class TLObject: if isinstance(data, str): data = data.encode('utf-8') else: - raise ValueError('bytes or str expected, not', type(data)) + raise TypeError( + 'bytes or str expected, not {}'.format(type(data))) r = [] if len(data) < 254: diff --git a/telethon/utils.py b/telethon/utils.py index 5e92b13d..388af83e 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -67,13 +67,13 @@ def get_extension(media): def _raise_cast_fail(entity, target): - raise ValueError('Cannot cast {} to any kind of {}.' - .format(type(entity).__name__, target)) + raise TypeError('Cannot cast {} to any kind of {}.'.format( + type(entity).__name__, target)) def get_input_peer(entity, allow_self=True): """Gets the input peer for the given "entity" (user, chat or channel). - A ValueError is raised if the given entity isn't a supported type.""" + A TypeError is raised if the given entity isn't a supported type.""" if not isinstance(entity, TLObject): _raise_cast_fail(entity, 'InputPeer') From ab07f0220a6646379fa27d840d8129d41b0248cb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 01:04:11 +0100 Subject: [PATCH 027/361] Save dc_id instead layer and salt in the session file Server salts change every 30 minutes after all, so keeping them in the long-term storage session file doesn't make much sense. Saving the layer doesn't make sense either, as it was only used to know whether to init connection or not, but it should be done always. --- telethon/telegram_bare_client.py | 26 +++++------- telethon/tl/session.py | 62 +++++++---------------------- telethon_tests/higher_level_test.py | 2 +- 3 files changed, 26 insertions(+), 64 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index d4f19b8d..d8cc498e 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -39,6 +39,7 @@ from .update_state import UpdateState from .utils import get_appropriated_part_size +DEFAULT_DC_ID = 4 DEFAULT_IPV4_IP = '149.154.167.51' DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]' DEFAULT_PORT = 443 @@ -101,9 +102,11 @@ class TelegramBareClient: # ':' in session.server_address is True if it's an IPv6 address if (not session.server_address or (':' in session.server_address) != use_ipv6): - session.port = DEFAULT_PORT - session.server_address = \ - DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP + session.set_dc( + DEFAULT_DC_ID, + DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP, + DEFAULT_PORT + ) self.session = session self.api_id = int(api_id) @@ -294,8 +297,7 @@ class TelegramBareClient: dc = self._get_dc(new_dc) __log__.info('Reconnecting to new data center %s', dc) - self.session.server_address = dc.ip_address - self.session.port = dc.port + self.session.set_dc(dc.id, dc.ip_address, dc.port) # auth_key's are associated with a server, which has now changed # so it's not valid anymore. Set to None to force recreating it. self.session.auth_key = None @@ -363,8 +365,7 @@ class TelegramBareClient: # Construct this session with the connection parameters # (system version, device model...) from the current one. session = Session(self.session) - session.server_address = dc.ip_address - session.port = dc.port + session.set_dc(dc.id, dc.ip_address, dc.port) self._exported_sessions[dc_id] = session __log__.info('Creating exported new client') @@ -390,8 +391,7 @@ class TelegramBareClient: if not session: dc = self._get_dc(cdn_redirect.dc_id, cdn=True) session = Session(self.session) - session.server_address = dc.ip_address - session.port = dc.port + session.set_dc(dc.id, dc.ip_address, dc.port) self._exported_sessions[cdn_redirect.dc_id] = session __log__.info('Creating new CDN client') @@ -494,7 +494,7 @@ class TelegramBareClient: def _invoke(self, sender, call_receive, update_state, *requests): # We need to specify the new layer (by initializing a new # connection) if it has changed from the latest known one. - init_connection = self.session.layer != LAYER + init_connection = False # TODO Only first call try: # Ensure that we start with no previous errors (i.e. resending) @@ -553,12 +553,6 @@ class TelegramBareClient: # User never called .connect(), so raise this error. raise - if init_connection: - # We initialized the connection successfully, even if - # a request had an RPC error we have invoked it fine. - self.session.layer = LAYER - self.session.save() - try: raise next(x.rpc_error for x in requests if x.rpc_error) except StopIteration: diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 3dfba1d9..030b4e13 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -67,6 +67,7 @@ class Session: self._sequence = 0 self.time_offset = 0 self._last_msg_id = 0 # Long + self.salt = 0 # Long # Cross-thread safety self._seq_no_lock = Lock() @@ -74,11 +75,10 @@ class Session: self._db_lock = Lock() # These values will be saved + self._dc_id = 0 self._server_address = None self._port = None self._auth_key = None - self._layer = 0 - self._salt = 0 # Signed long # Migrating from .json -> SQL entities = self._check_migrate_json() @@ -97,8 +97,7 @@ class Session: # These values will be saved c.execute('select * from sessions') - self._server_address, self._port, key, \ - self._layer, self._salt = c.fetchone() + self._dc_id, self._server_address, self._port, key, = c.fetchone() from ..crypto import AuthKey self._auth_key = AuthKey(data=key) @@ -108,12 +107,11 @@ class Session: c.execute("create table version (version integer)") c.execute( """create table sessions ( + dc_id integer primary key, server_address text, port integer, - auth_key blob, - layer integer, - salt integer - )""" + auth_key blob + ) without rowid""" ) c.execute( """create table entities ( @@ -142,13 +140,6 @@ class Session: self.delete() # Delete JSON file to create database self._port = data.get('port', self._port) - self._salt = data.get('salt', self._salt) - # Keep while migrating from unsigned to signed salt - if self._salt > 0: - self._salt = struct.unpack( - 'q', struct.pack('Q', self._salt))[0] - - self._layer = data.get('layer', self._layer) self._server_address = \ data.get('server_address', self._server_address) @@ -169,24 +160,20 @@ class Session: # Data from sessions should be kept as properties # not to fetch the database every time we need it + def set_dc(self, dc_id, server_address, port): + self._dc_id = dc_id + self._server_address = server_address + self._port = port + self._update_session_table() + @property def server_address(self): return self._server_address - @server_address.setter - def server_address(self, value): - self._server_address = value - self._update_session_table() - @property def port(self): return self._port - @port.setter - def port(self, value): - self._port = value - self._update_session_table() - @property def auth_key(self): return self._auth_key @@ -196,34 +183,15 @@ class Session: self._auth_key = value self._update_session_table() - @property - def layer(self): - return self._layer - - @layer.setter - def layer(self, value): - self._layer = value - self._update_session_table() - - @property - def salt(self): - return self._salt - - @salt.setter - def salt(self, value): - self._salt = value - self._update_session_table() - def _update_session_table(self): with self._db_lock: c = self._conn.cursor() c.execute('delete from sessions') - c.execute('insert into sessions values (?,?,?,?,?)', ( + c.execute('insert into sessions values (?,?,?,?)', ( + self._dc_id, self._server_address, self._port, - self._auth_key.key if self._auth_key else b'', - self._layer, - self._salt + self._auth_key.key if self._auth_key else b'' )) c.close() diff --git a/telethon_tests/higher_level_test.py b/telethon_tests/higher_level_test.py index 7bd4b181..7433fac9 100644 --- a/telethon_tests/higher_level_test.py +++ b/telethon_tests/higher_level_test.py @@ -18,7 +18,7 @@ class HigherLevelTests(unittest.TestCase): @staticmethod def test_cdn_download(): client = TelegramClient(None, api_id, api_hash) - client.session.server_address = '149.154.167.40' + client.session.set_dc(0, '149.154.167.40', 80) assert client.connect() try: From 2a10f315119283b2976261f2585551610b680320 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 01:13:24 +0100 Subject: [PATCH 028/361] Always wrap init connection for first call Ping @delivrance. See https://core.telegram.org/api/invoking#saving-client-info. --- telethon/telegram_bare_client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index d8cc498e..55ac6c41 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -154,6 +154,10 @@ class TelegramBareClient: # Save whether the user is authorized here (a.k.a. logged in) self._authorized = None # None = We don't know yet + # The first request must be in invokeWithLayer(initConnection(X)). + # See https://core.telegram.org/api/invoking#saving-client-info. + self._first_request = True + # Uploaded files cache so subsequent calls are instant self._upload_cache = {} @@ -268,7 +272,7 @@ class TelegramBareClient: self._recv_thread.join() # TODO Shall we clear the _exported_sessions, or may be reused? - pass + self._first_request = True # On reconnect it will be first again def _reconnect(self, new_dc=None): """If 'new_dc' is not set, only a call to .connect() will be made @@ -492,10 +496,6 @@ class TelegramBareClient: invoke = __call__ def _invoke(self, sender, call_receive, update_state, *requests): - # We need to specify the new layer (by initializing a new - # connection) if it has changed from the latest known one. - init_connection = False # TODO Only first call - try: # Ensure that we start with no previous errors (i.e. resending) for x in requests: @@ -503,14 +503,11 @@ class TelegramBareClient: x.rpc_error = None if not self.session.auth_key: - # New key, we need to tell the server we're going to use - # the latest layer and initialize the connection doing so. __log__.info('Need to generate new auth key before invoking') self.session.auth_key, self.session.time_offset = \ authenticator.do_authentication(self._sender.connection) - init_connection = True - if init_connection: + if self._first_request: __log__.info('Initializing a new connection while invoking') if len(requests) == 1: requests = [self._wrap_init_connection(requests[0])] @@ -553,6 +550,9 @@ class TelegramBareClient: # User never called .connect(), so raise this error. raise + # Clear the flag if we got this far + self._first_request = False + try: raise next(x.rpc_error for x in requests if x.rpc_error) except StopIteration: From 0755bda2208360c496c264b7840e6d16299f4a0e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 02:01:22 +0100 Subject: [PATCH 029/361] Stop returning tuples off .get_message_history() Now the information is saved in the modified Message instances, which makes it easier to use (message.sender, message.to...) --- telethon/telegram_client.py | 47 ++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 2f48f7a7..a5ea1025 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -484,8 +484,12 @@ class TelegramClient(TelegramBareClient): Additional message offset (all of the specified offsets + this offset = older messages). - :return: A tuple containing total message count and two more lists ([messages], [senders]). - Note that the sender can be null if it was not found! + :return: A list of messages with extra attributes: + .total = total amount of messages in this history + .sender = entity of the sender + .fwd_from.sender = if fwd_from, who sent it originally + .fwd_from.channel = if fwd_from, original channel + .to = entity to which the message was sent """ entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) @@ -537,25 +541,30 @@ class TelegramClient(TelegramBareClient): if limit > 3000: time.sleep(1) - # In a new list with the same length as the messages append - # their senders, so people can zip(messages, senders). - senders = [] + # Add a few extra attributes to the Message to make it friendlier. for m in messages: - if m.from_id: - who = entities[utils.get_peer_id(m.from_id, add_mark=True)] - elif getattr(m, 'fwd_from', None): - # .from_id is optional, so this is the sanest fallback. - who = entities[utils.get_peer_id( - m.fwd_from.from_id or PeerChannel(m.fwd_from.channel_id), - add_mark=True - )] - else: - # If there's not even a FwdHeader, fallback to the sender - # being where the message was sent. - who = entities[utils.get_peer_id(m.to_id, add_mark=True)] - senders.append(who) + # TODO Better way to return a total without tuples? + m.total = total_messages + m.sender = (None if not m.from_id else + entities[utils.get_peer_id(m.from_id, add_mark=True)]) - return total_messages, messages, senders + if getattr(m, 'fwd_from', None): + m.fwd_from.sender = ( + None if not m.fwd_from.from_id else + entities[utils.get_peer_id( + m.fwd_from.from_id, add_mark=True + )] + ) + m.fwd_from.channel = ( + None if not m.fwd_from.channel_id else + entities[utils.get_peer_id( + PeerChannel(m.fwd_from.channel_id), add_mark=True + )] + ) + + m.to = entities[utils.get_peer_id(m.to_id, add_mark=True)] + + return messages def send_read_acknowledge(self, entity, message=None, max_id=None): """ From 459022bdabba3cefe5db09fb8da81eca2cb65156 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 11:49:35 +0100 Subject: [PATCH 030/361] Return a UserList with a .total attribute for get dialogs/history --- telethon/telegram_client.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a5ea1025..b59f8705 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,7 +1,7 @@ import itertools import os import time -from collections import OrderedDict +from collections import OrderedDict, UserList from datetime import datetime, timedelta from mimetypes import guess_type @@ -296,12 +296,23 @@ class TelegramClient(TelegramBareClient): :param offset_peer: The peer to be used as an offset. - :return List[telethon.tl.custom.Dialog]: A list dialogs. + :return UserList[telethon.tl.custom.Dialog]: + A list dialogs, with an additional .total attribute on the list. """ limit = float('inf') if limit is None else int(limit) if limit == 0: - return [] + # Special case, get a single dialog and determine count + dialogs = self(GetDialogsRequest( + offset_date=offset_date, + offset_id=offset_id, + offset_peer=offset_peer, + limit=1 + )) + result = UserList() + result.total = getattr(dialogs, 'count', len(dialogs.dialogs)) + return result + total_count = 0 dialogs = OrderedDict() # Use peer id as identifier to avoid dupes while len(dialogs) < limit: real_limit = min(limit - len(dialogs), 100) @@ -312,6 +323,7 @@ class TelegramClient(TelegramBareClient): limit=real_limit )) + total_count = getattr(r, 'count', len(r.dialogs)) messages = {m.id: m for m in r.messages} entities = {utils.get_peer_id(x, add_mark=True): x for x in itertools.chain(r.users, r.chats)} @@ -331,7 +343,8 @@ class TelegramClient(TelegramBareClient): ) offset_id = r.messages[-1].id & 4294967296 # Telegram/danog magic - dialogs = list(dialogs.values()) + dialogs = UserList(dialogs.values()) + dialogs.total = total_count return dialogs[:limit] if limit < float('inf') else dialogs def get_drafts(self): # TODO: Ability to provide a `filter` @@ -485,7 +498,7 @@ class TelegramClient(TelegramBareClient): (all of the specified offsets + this offset = older messages). :return: A list of messages with extra attributes: - .total = total amount of messages in this history + .total = (on the list) total amount of messages sent .sender = entity of the sender .fwd_from.sender = if fwd_from, who sent it originally .fwd_from.channel = if fwd_from, original channel @@ -502,7 +515,7 @@ class TelegramClient(TelegramBareClient): return getattr(result, 'count', len(result.messages)), [], [] total_messages = 0 - messages = [] + messages = UserList() entities = {} while len(messages) < limit: # Telegram has a hard limit of 100 @@ -542,9 +555,9 @@ class TelegramClient(TelegramBareClient): time.sleep(1) # Add a few extra attributes to the Message to make it friendlier. + messages.total = total_messages for m in messages: # TODO Better way to return a total without tuples? - m.total = total_messages m.sender = (None if not m.from_id else entities[utils.get_peer_id(m.from_id, add_mark=True)]) From bfff1567aff5889e1069c9be2d116ce7c2dc39eb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 11:55:05 +0100 Subject: [PATCH 031/361] Fix up some mismatching raise/except types since 6ec6967 --- telethon/telegram_client.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index b59f8705..b5f85fd3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -30,7 +30,7 @@ from .tl.functions.contacts import ( GetContactsRequest, ResolveUsernameRequest ) from .tl.functions.messages import ( - GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest, + GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, CheckChatInviteRequest ) @@ -1091,17 +1091,11 @@ class TelegramClient(TelegramBareClient): an username, and processes all the found entities on the session. The string may also be a user link, or a channel/chat invite link. -<<<<<<< HEAD This method has the side effect of adding the found users to the session database, so it can be queried later without API calls, if this option is enabled on the session. -======= - raise TypeError( - 'Cannot turn "{}" into any entity (user or chat)'.format(entity) - ) ->>>>>>> 6ec6967ff9a2e09aae70b500273075bdfbae975c - Returns the found entity. + Returns the found entity, or raises TypeError if not found. """ phone = utils.parse_phone(string) if phone: @@ -1125,6 +1119,10 @@ class TelegramClient(TelegramBareClient): if entity.username.lower() == string: return entity + raise TypeError( + 'Cannot turn "{}" into any entity (user or chat)'.format(string) + ) + def get_input_entity(self, peer): """ Turns the given peer into its input entity version. Most requests @@ -1164,7 +1162,7 @@ class TelegramClient(TelegramBareClient): if not is_peer: try: return utils.get_input_peer(peer) - except ValueError: + except TypeError: pass if not is_peer: @@ -1189,7 +1187,7 @@ class TelegramClient(TelegramBareClient): if utils.get_peer_id(entity, add_mark=True) == target: return utils.get_input_peer(entity) - raise ValueError( + raise TypeError( 'Could not find the input entity corresponding to "{}".' 'Make sure you have encountered this peer before.'.format(peer) ) From 75a342e24ba431fcbacfd47073edff4ec97f3114 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 12:11:31 +0100 Subject: [PATCH 032/361] Fix .download_media() not handling Photo (closes #473) --- telethon/telegram_client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index b5f85fd3..77a71537 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -52,7 +52,7 @@ from .tl.types import ( InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, - ChatInvite, ChatInviteAlready, PeerChannel + ChatInvite, ChatInviteAlready, PeerChannel, Photo ) from .tl.types.messages import DialogsSlice from .extensions import markdown @@ -848,7 +848,7 @@ class TelegramClient(TelegramBareClient): date = datetime.now() media = message - if isinstance(media, MessageMediaPhoto): + if isinstance(media, (MessageMediaPhoto, Photo)): return self._download_photo( media, file, date, progress_callback ) @@ -861,11 +861,15 @@ class TelegramClient(TelegramBareClient): media, file ) - def _download_photo(self, mm_photo, file, date, progress_callback): + def _download_photo(self, photo, file, date, progress_callback): """Specialized version of .download_media() for photos""" # Determine the photo and its largest size - photo = getattr(mm_photo, 'photo', mm_photo) + if isinstance(photo, MessageMediaPhoto): + photo = photo.photo + if not isinstance(photo, Photo): + return + largest_size = photo.sizes[-1] file_size = largest_size.size largest_size = largest_size.location From 3537e9bcc9060e45f8d4d6714875c23608c7afc5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 12:32:16 +0100 Subject: [PATCH 033/361] Support more types to represent a date --- telethon/tl/tlobject.py | 20 +++++++++++++++++++- telethon_generator/tl_generator.py | 6 +----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 489765e2..0ed7b015 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -1,4 +1,5 @@ -from datetime import datetime +import struct +from datetime import datetime, date from threading import Event @@ -125,6 +126,23 @@ class TLObject: r.append(bytes(padding)) return b''.join(r) + @staticmethod + def serialize_datetime(dt): + if not dt: + return b'\0\0\0\0' + + if isinstance(dt, datetime): + dt = int(dt.timestamp()) + elif isinstance(dt, date): + dt = int(datetime(dt.year, dt.month, dt.day, dt).timestamp()) + elif isinstance(dt, float): + dt = int(dt) + + if isinstance(dt, int): + return struct.pack(' Date: Thu, 28 Dec 2017 12:43:50 +0100 Subject: [PATCH 034/361] Update to v0.16 --- telethon/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/version.py b/telethon/version.py index 096fbd6c..e7fcc442 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.15.5' +__version__ = '0.16' From 7ed3be8e6f0ec053338e1dc4f936430f4b07aedf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 13:21:35 +0100 Subject: [PATCH 035/361] Fix .get_dialogs() failing due to IDs being marked Also removed utils.find_user_or_chat to prevent this from happening again. Using a dict {marked_id: entity} is better. --- telethon/telegram_client.py | 5 ++--- telethon/utils.py | 22 ---------------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 77a71537..c508ad52 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -338,9 +338,8 @@ class TelegramClient(TelegramBareClient): break offset_date = r.messages[-1].date - offset_peer = utils.find_user_or_chat( - r.dialogs[-1].peer, entities, entities - ) + offset_peer = entities[ + utils.get_peer_id(r.dialogs[-1].peer, add_mark=True)] offset_id = r.messages[-1].id & 4294967296 # Telegram/danog magic dialogs = UserList(dialogs.values()) diff --git a/telethon/utils.py b/telethon/utils.py index 720345db..531b0dc7 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -386,28 +386,6 @@ def resolve_id(marked_id): return -marked_id, PeerChat -def find_user_or_chat(peer, users, chats): - """Finds the corresponding user or chat given a peer. - Returns None if it was not found""" - if isinstance(peer, PeerUser): - peer, where = peer.user_id, users - else: - where = chats - if isinstance(peer, PeerChat): - peer = peer.chat_id - elif isinstance(peer, PeerChannel): - peer = peer.channel_id - - if isinstance(peer, int): - if isinstance(where, dict): - return where.get(peer) - else: - try: - return next(x for x in where if x.id == peer) - except StopIteration: - pass - - def get_appropriated_part_size(file_size): """Gets the appropriated part size when uploading or downloading files, given an initial file size""" From 55b67b65a1026bb874598f75186a10bc204f0c90 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 13:31:43 +0100 Subject: [PATCH 036/361] Remove optional add_mark parameter from .get_peer_id It was always True after all, and it made no sense for it to be False. --- telethon/telegram_client.py | 29 +++++++++++++---------------- telethon/tl/custom/dialog.py | 2 +- telethon/tl/session.py | 4 ++-- telethon/utils.py | 22 ++++++++++++---------- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c508ad52..3b17e4c2 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -325,11 +325,11 @@ class TelegramClient(TelegramBareClient): total_count = getattr(r, 'count', len(r.dialogs)) messages = {m.id: m for m in r.messages} - entities = {utils.get_peer_id(x, add_mark=True): x + entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} for d in r.dialogs: - dialogs[utils.get_peer_id(d.peer, add_mark=True)] = \ + dialogs[utils.get_peer_id(d.peer)] = \ Dialog(self, d, entities, messages) if len(r.dialogs) < real_limit or not isinstance(r, DialogsSlice): @@ -338,8 +338,7 @@ class TelegramClient(TelegramBareClient): break offset_date = r.messages[-1].date - offset_peer = entities[ - utils.get_peer_id(r.dialogs[-1].peer, add_mark=True)] + offset_peer = entities[utils.get_peer_id(r.dialogs[-1].peer)] offset_id = r.messages[-1].id & 4294967296 # Telegram/danog magic dialogs = UserList(dialogs.values()) @@ -536,9 +535,9 @@ class TelegramClient(TelegramBareClient): # TODO We can potentially use self.session.database, but since # it might be disabled, use a local dictionary. for u in result.users: - entities[utils.get_peer_id(u, add_mark=True)] = u + entities[utils.get_peer_id(u)] = u for c in result.chats: - entities[utils.get_peer_id(c, add_mark=True)] = c + entities[utils.get_peer_id(c)] = c if len(result.messages) < real_limit: break @@ -558,23 +557,21 @@ class TelegramClient(TelegramBareClient): for m in messages: # TODO Better way to return a total without tuples? m.sender = (None if not m.from_id else - entities[utils.get_peer_id(m.from_id, add_mark=True)]) + entities[utils.get_peer_id(m.from_id)]) if getattr(m, 'fwd_from', None): m.fwd_from.sender = ( None if not m.fwd_from.from_id else - entities[utils.get_peer_id( - m.fwd_from.from_id, add_mark=True - )] + entities[utils.get_peer_id(m.fwd_from.from_id)] ) m.fwd_from.channel = ( None if not m.fwd_from.channel_id else entities[utils.get_peer_id( - PeerChannel(m.fwd_from.channel_id), add_mark=True + PeerChannel(m.fwd_from.channel_id) )] ) - m.to = entities[utils.get_peer_id(m.to_id, add_mark=True)] + m.to = entities[utils.get_peer_id(m.to_id)] return messages @@ -1073,7 +1070,7 @@ class TelegramClient(TelegramBareClient): # Merge users, chats and channels into a single dictionary id_entity = { - utils.get_peer_id(x, add_mark=True): x + utils.get_peer_id(x): x for x in itertools.chain(users, chats, channels) } @@ -1083,7 +1080,7 @@ class TelegramClient(TelegramBareClient): # username changes. result = [ self._get_entity_from_string(x) if isinstance(x, str) - else id_entity[utils.get_peer_id(x, add_mark=True)] + else id_entity[utils.get_peer_id(x)] for x in inputs ] return result[0] if single else result @@ -1185,9 +1182,9 @@ class TelegramClient(TelegramBareClient): exclude_pinned=True )) - target = utils.get_peer_id(peer, add_mark=True) + target = utils.get_peer_id(peer) for entity in itertools.chain(dialogs.users, dialogs.chats): - if utils.get_peer_id(entity, add_mark=True) == target: + if utils.get_peer_id(entity) == target: return utils.get_input_peer(entity) raise TypeError( diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index bac8b0de..fd36ba8f 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -17,7 +17,7 @@ class Dialog: self.message = messages.get(dialog.top_message, None) self.date = getattr(self.message, 'date', None) - self.entity = entities[utils.get_peer_id(dialog.peer, add_mark=True)] + self.entity = entities[utils.get_peer_id(dialog.peer)] self.input_entity = utils.get_input_peer(self.entity) self.name = utils.get_display_name(self.entity) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 030b4e13..bb38f489 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -285,7 +285,7 @@ class Session: continue try: p = utils.get_input_peer(e, allow_self=False) - marked_id = utils.get_peer_id(p, add_mark=True) + marked_id = utils.get_peer_id(p) except ValueError: continue @@ -327,7 +327,7 @@ class Session: key = utils.get_input_peer(key) if type(key).SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') return key - key = utils.get_peer_id(key, add_mark=True) + key = utils.get_peer_id(key) c = self._conn.cursor() if isinstance(key, str): diff --git a/telethon/utils.py b/telethon/utils.py index 531b0dc7..48c867d1 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -338,9 +338,14 @@ def parse_username(username): return username.lower(), 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. +def get_peer_id(peer): + """ + Finds the ID of the given peer, and converts it to the "bot api" format + so it the peer can be identified back. User ID is left unmodified, + chat ID is negated, and channel ID is prefixed with -100. + + The original ID and the peer type class can be returned with + a call to utils.resolve_id(marked_id). """ # First we assert it's a Peer TLObject, or early return for integers if not isinstance(peer, TLObject): @@ -357,7 +362,7 @@ def get_peer_id(peer, add_mark=False): if isinstance(peer, (PeerUser, InputPeerUser)): return peer.user_id elif isinstance(peer, (PeerChat, InputPeerChat)): - return -peer.chat_id if add_mark else peer.chat_id + return -peer.chat_id elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)): if isinstance(peer, ChannelFull): # Special case: .get_input_peer can't return InputChannel from @@ -365,12 +370,9 @@ def get_peer_id(peer, add_mark=False): i = peer.id else: i = peer.channel_id - if add_mark: - # Concat -100 through math tricks, .to_supergroup() on Madeline - # IDs will be strictly positive -> log works - return -(i + pow(10, math.floor(math.log10(i) + 3))) - else: - return i + # Concat -100 through math tricks, .to_supergroup() on Madeline + # IDs will be strictly positive -> log works + return -(i + pow(10, math.floor(math.log10(i) + 3))) _raise_cast_fail(peer, 'int') From 50d413b1c93119635b8e27efc8b77a8f9683438a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 14:55:02 +0100 Subject: [PATCH 037/361] Fix slicing dialogs was turning UserList into list --- telethon/telegram_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3b17e4c2..72f9f98b 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -341,9 +341,11 @@ class TelegramClient(TelegramBareClient): offset_peer = entities[utils.get_peer_id(r.dialogs[-1].peer)] offset_id = r.messages[-1].id & 4294967296 # Telegram/danog magic - dialogs = UserList(dialogs.values()) + dialogs = UserList( + itertools.islice(dialogs.values(), min(limit, len(dialogs))) + ) dialogs.total = total_count - return dialogs[:limit] if limit < float('inf') else dialogs + return dialogs def get_drafts(self): # TODO: Ability to provide a `filter` """ From 4a139b0ae499f3d3026010a9392ee8a88bff0d54 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 14:58:42 +0100 Subject: [PATCH 038/361] Fix session table may be empty if no DC switch --- telethon/tl/session.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index bb38f489..26c9576e 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -97,10 +97,12 @@ class Session: # These values will be saved c.execute('select * from sessions') - self._dc_id, self._server_address, self._port, key, = c.fetchone() + tuple_ = c.fetchone() + if tuple_: + self._dc_id, self._server_address, self._port, key, = tuple_ + from ..crypto import AuthKey + self._auth_key = AuthKey(data=key) - from ..crypto import AuthKey - self._auth_key = AuthKey(data=key) c.close() else: # Tables don't exist, create new ones From ea436a4fac307336251429470cd623e829ef9681 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 16:25:41 +0100 Subject: [PATCH 039/361] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 21e76aca..c4b9b7e8 100755 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ Doing stuff client.send_file('username', '/home/myself/Pictures/holidays.jpg') client.download_profile_photo(me) - total, messages, senders = client.get_message_history('username') + messages = client.get_message_history('username') client.download_media(messages[0]) From 47b53ce89f529128ddc74ad45dec84b67a0c0b7a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 28 Dec 2017 17:06:14 +0100 Subject: [PATCH 040/361] Except only UnicodeDecodeError to check migration (fix #511) --- telethon/tl/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 26c9576e..236c1096 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -154,7 +154,7 @@ class Session: for p_id, p_hash in data.get('entities', []): rows.append((p_id, p_hash, None, None, None)) return rows - except (UnicodeDecodeError, json.decoder.JSONDecodeError): + except UnicodeDecodeError: return [] # No entities def _upgrade_database(self, old): From 0570c55120a3a764c4a3649bdc41acc5736f970d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 29 Dec 2017 00:43:52 +0100 Subject: [PATCH 041/361] Remove hardcoded database version from session sql statement --- telethon/tl/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 236c1096..193c6d44 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -107,6 +107,7 @@ class Session: else: # Tables don't exist, create new ones c.execute("create table version (version integer)") + c.execute("insert into version values (?)", (CURRENT_VERSION,)) c.execute( """create table sessions ( dc_id integer primary key, @@ -124,7 +125,6 @@ class Session: name text ) without rowid""" ) - c.execute("insert into version values (1)") # Migrating from JSON -> new table and may have entities if entities: c.executemany( From d2121c76cbb6db4574f9ef3e88ecb96c891f3a02 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 29 Dec 2017 19:41:12 +0100 Subject: [PATCH 042/361] Fetch and persist each auth_key per DC --- telethon/tl/session.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 193c6d44..8c2850bf 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -168,6 +168,17 @@ class Session: self._port = port self._update_session_table() + # Fetch the auth_key corresponding to this data center + c = self._conn.cursor() + c.execute('select auth_key from sessions') + tuple_ = c.fetchone() + if tuple_: + from ..crypto import AuthKey + self._auth_key = AuthKey(data=tuple_[0]) + else: + self._auth_key = None + c.close() + @property def server_address(self): return self._server_address @@ -188,8 +199,7 @@ class Session: def _update_session_table(self): with self._db_lock: c = self._conn.cursor() - c.execute('delete from sessions') - c.execute('insert into sessions values (?,?,?,?)', ( + c.execute('insert or replace into sessions values (?,?,?,?)', ( self._dc_id, self._server_address, self._port, From cbf6306599115ad7a130eee07a8bf7106caef683 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 29 Dec 2017 22:07:16 +0100 Subject: [PATCH 043/361] Fix early cast to input from 932ed9e causing error on Peer --- telethon/tl/session.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 8c2850bf..3fa13d23 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -336,10 +336,12 @@ class Session: Raises ValueError if it cannot be found. """ if isinstance(key, TLObject): - key = utils.get_input_peer(key) - if type(key).SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') - return key - key = utils.get_peer_id(key) + try: + # Try to early return if this key can be casted as input peer + return utils.get_input_peer(key) + except TypeError: + # Otherwise, get the ID of the peer + key = utils.get_peer_id(key) c = self._conn.cursor() if isinstance(key, str): From 6eef6f5d239e554697a69f44eb278b330a18cbe2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 2 Jan 2018 00:02:31 +0100 Subject: [PATCH 044/361] Update to layer 74 --- telethon_generator/scheme.tl | 45 +++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index 2ecb31b4..1d03c281 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -166,11 +166,9 @@ inputMediaGifExternal#4843b0fd url:string q:string = InputMedia; inputMediaPhotoExternal#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; -inputMediaInvoice#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia; +inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia; inputMediaGeoLive#7b1a118f geo_point:InputGeoPoint period:int = InputMedia; -inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia; - inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto; inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto; @@ -345,6 +343,7 @@ messages.dialogsSlice#71e094f3 count:int dialogs:Vector messages:Vector< messages.messages#8c718e87 messages:Vector chats:Vector users:Vector = messages.Messages; messages.messagesSlice#b446ae3 count:int messages:Vector chats:Vector users:Vector = messages.Messages; messages.channelMessages#99262e37 flags:# pts:int count:int messages:Vector chats:Vector users:Vector = messages.Messages; +messages.messagesNotModified#74535f21 count:int = messages.Messages; messages.chats#64ff9fd5 chats:Vector = messages.Chats; messages.chatsSlice#9cd81144 count:int chats:Vector = messages.Chats; @@ -357,7 +356,6 @@ inputMessagesFilterEmpty#57e2f66c = MessagesFilter; inputMessagesFilterPhotos#9609a51c = MessagesFilter; inputMessagesFilterVideo#9fc00e65 = MessagesFilter; inputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter; -inputMessagesFilterPhotoVideoDocuments#d95e73bb = MessagesFilter; inputMessagesFilterDocument#9eddf188 = MessagesFilter; inputMessagesFilterUrl#7ef0dd87 = MessagesFilter; inputMessagesFilterGif#ffc86587 = MessagesFilter; @@ -368,8 +366,8 @@ inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFil inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter; inputMessagesFilterRoundVideo#b549da53 = MessagesFilter; inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter; -inputMessagesFilterContacts#e062db83 = MessagesFilter; inputMessagesFilterGeo#e7026d0d = MessagesFilter; +inputMessagesFilterContacts#e062db83 = MessagesFilter; updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; updateMessageID#4e90bfd6 id:int random_id:long = Update; @@ -463,7 +461,7 @@ upload.fileCdnRedirect#ea52fe5a dc_id:int file_token:bytes encryption_key:bytes dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int = DcOption; -config#9c840964 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector = Config; +config#9c840964 flags:# phonecalls_enabled:flags.1?true default_p2p_contacts:flags.3?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector = Config; nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; @@ -524,7 +522,7 @@ sendMessageGamePlayAction#dd6a8f48 = SendMessageAction; sendMessageRecordRoundAction#88f27fbc = SendMessageAction; sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; -contacts.found#1aa1f784 results:Vector chats:Vector users:Vector = contacts.Found; +contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; inputPrivacyKeyStatusTimestamp#4f96cb18 = InputPrivacyKey; inputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey; @@ -723,7 +721,7 @@ auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType; auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType; auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType; -messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer; +messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer; messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData; @@ -825,7 +823,7 @@ dataJSON#7d748d04 data:string = DataJSON; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; -invoice#c30aa358 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true currency:string prices:Vector = Invoice; +invoice#c30aa358 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true currency:string prices:Vector = Invoice; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; @@ -856,6 +854,8 @@ payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_inf inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials; inputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials; +inputPaymentCredentialsApplePay#aa1c39f payment_data:DataJSON = InputPaymentCredentials; +inputPaymentCredentialsAndroidPay#ca05d50e payment_token:DataJSON google_transaction_id:string = InputPaymentCredentials; account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword; @@ -893,7 +893,7 @@ langPackDifference#f385c1f6 lang_code:string from_version:int version:int string langPackLanguage#117698f1 name:string native_name:string lang_code:string = LangPackLanguage; -channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true = ChannelAdminRights; +channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true manage_call:flags.10?true = ChannelAdminRights; channelBannedRights#58cf4249 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true until_date:int = ChannelBannedRights; @@ -927,13 +927,15 @@ cdnFileHash#77eec38f offset:int limit:int hash:bytes = CdnFileHash; messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers; messages.favedStickers#f37f2f16 hash:int packs:Vector stickers:Vector = messages.FavedStickers; -help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; - +recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl; recentMeUrlUser#8dbc3336 url:string user_id:int = RecentMeUrl; recentMeUrlChat#a01b22f9 url:string chat_id:int = RecentMeUrl; -recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl; -recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl; +recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; + +help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; + +inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia; ---functions--- @@ -961,8 +963,8 @@ auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentC auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool; -account.registerDevice#637ea878 token_type:int token:string = Bool; -account.unregisterDevice#65c55b40 token_type:int token:string = Bool; +account.registerDevice#f75874d1 token_type:int token:string other_uids:Vector = Bool; +account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector = Bool; account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool; account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings; account.resetNotifySettings#db7e1747 = Bool; @@ -1010,7 +1012,7 @@ contacts.resetSaved#879537f1 = Bool; messages.getMessages#4222fa74 id:Vector = messages.Messages; messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs; -messages.getHistory#afa92846 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; +messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages; messages.search#39e9ea0 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages; messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true peer:InputPeer max_id:int = messages.AffectedHistory; @@ -1067,7 +1069,7 @@ messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags messages.sendInlineBotResult#b16e06fe flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; messages.editMessage#5d1b8dd flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector geo_point:flags.13?InputGeoPoint = Updates; -messages.editInlineBotMessage#130c2c85 flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; +messages.editInlineBotMessage#b0e08243 flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector geo_point:flags.13?InputGeoPoint = Bool; messages.getBotCallbackAnswer#810a9fec flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; messages.getPeerDialogs#2d9776b9 peers:Vector = messages.PeerDialogs; @@ -1098,9 +1100,10 @@ messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers; messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; -messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages; messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory; +messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages; messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector = Updates; +messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1153,7 +1156,7 @@ channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector = channels.exportInvite#c7560885 channel:InputChannel = ExportedChatInvite; channels.deleteChannel#c0111fe3 channel:InputChannel = Updates; channels.toggleInvites#49609307 channel:InputChannel enabled:Bool = Updates; -channels.exportMessageLink#c846d22d channel:InputChannel id:int = ExportedMessageLink; +channels.exportMessageLink#ceb77163 channel:InputChannel id:int grouped:Bool = ExportedMessageLink; channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates; channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates; channels.getAdminedPublicChannels#8d8d82d7 = messages.Chats; @@ -1193,4 +1196,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector = Vector; -// LAYER 73 +// LAYER 74 From 33d6afa0bdef17931672806d519792a0a4578eda Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 3 Jan 2018 19:18:24 +0100 Subject: [PATCH 045/361] Add missing L74 hash parameter to .get_message_history() --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 72f9f98b..70c2784c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -527,7 +527,8 @@ class TelegramClient(TelegramBareClient): offset_id=offset_id, max_id=max_id, min_id=min_id, - add_offset=add_offset + add_offset=add_offset, + hash=0 )) messages.extend( m for m in result.messages if not isinstance(m, MessageEmpty) From b9cd9a66396f272216ef52feabe7a43366bd3d89 Mon Sep 17 00:00:00 2001 From: Csaba Henk Date: Tue, 2 Jan 2018 09:56:37 +0100 Subject: [PATCH 046/361] fix get_dialogs() return type in example Catching up with 238198db where get_dialogs return type was changed. --- telethon_examples/interactive_telegram_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 52c2c356..501d557b 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -138,15 +138,15 @@ class InteractiveTelegramClient(TelegramClient): # Entities represent the user, chat or channel # corresponding to the dialog on the same index. - dialogs, entities = self.get_dialogs(limit=dialog_count) + dialogs = self.get_dialogs(limit=dialog_count) i = None while i is None: print_title('Dialogs window') # Display them so the user can choose - for i, entity in enumerate(entities, start=1): - sprint('{}. {}'.format(i, get_display_name(entity))) + for i, dialog in enumerate(dialogs, start=1): + sprint('{}. {}'.format(i, get_display_name(dialog.entity))) # Let the user decide who they want to talk to print() @@ -177,7 +177,7 @@ class InteractiveTelegramClient(TelegramClient): i = None # Retrieve the selected user (or chat, or channel) - entity = entities[i] + entity = dialogs[i].entity # Show some information print_title('Chat with "{}"'.format(get_display_name(entity))) From 78871b697e9f6a330b5984a0d69231ca0f5aaa13 Mon Sep 17 00:00:00 2001 From: Csaba Henk Date: Tue, 2 Jan 2018 13:30:29 +0100 Subject: [PATCH 047/361] client: return the message in send_file, too --- telethon/telegram_client.py | 39 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 70c2784c..730f7445 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -363,6 +363,22 @@ class TelegramClient(TelegramBareClient): drafts = [Draft._from_update(self, u) for u in response.updates] return drafts + @staticmethod + def _get_response_message(request, result): + # Telegram seems to send updateMessageID first, then updateNewMessage, + # however let's not rely on that just in case. + msg_id = None + for update in result.updates: + if isinstance(update, UpdateMessageID): + if update.random_id == request.random_id: + msg_id = update.id + break + + for update in result.updates: + if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)): + if update.message.id == msg_id: + return update.message + def send_message(self, entity, message, @@ -415,21 +431,7 @@ class TelegramClient(TelegramBareClient): entities=result.entities ) - # Telegram seems to send updateMessageID first, then updateNewMessage, - # however let's not rely on that just in case. - msg_id = None - for update in result.updates: - if isinstance(update, UpdateMessageID): - if update.random_id == request.random_id: - msg_id = update.id - break - - for update in result.updates: - if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)): - if update.message.id == msg_id: - return update.message - - return None # Should not happen + return self._get_response_message(request, result) def delete_messages(self, entity, message_ids, revoke=True): """ @@ -723,11 +725,14 @@ class TelegramClient(TelegramBareClient): # Once the media type is properly specified and the file uploaded, # send the media message to the desired entity. - self(SendMediaRequest( + request = SendMediaRequest( peer=self.get_input_entity(entity), media=media, reply_to_msg_id=self._get_reply_to(reply_to) - )) + ) + result = self(request) + + return self._get_response_message(request, result) def send_voice_note(self, entity, file, caption='', upload_progress=None, reply_to=None): From 2c437c51bb672cd1eaa4155b205dcfa20f535eb8 Mon Sep 17 00:00:00 2001 From: Csaba Henk Date: Wed, 3 Jan 2018 12:47:38 +0100 Subject: [PATCH 048/361] client: add thumbnail support for send_file() --- telethon/telegram_client.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 730f7445..3be2ac62 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -631,6 +631,7 @@ class TelegramClient(TelegramBareClient): force_document=False, progress_callback=None, reply_to=None, attributes=None, + thumb=None, **kwargs): """ Sends a file to the specified entity. @@ -658,6 +659,8 @@ class TelegramClient(TelegramBareClient): :param attributes: Optional attributes that override the inferred ones, like DocumentAttributeFilename and so on. + :param thumb: + Optional thumbnail (for videos). :param kwargs: 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. @@ -716,11 +719,16 @@ class TelegramClient(TelegramBareClient): if not mime_type: mime_type = 'application/octet-stream' + input_kw = {} + if thumb: + input_kw['thumb'] = self.upload_file(thumb) + media = InputMediaUploadedDocument( file=file_handle, mime_type=mime_type, attributes=list(attr_dict.values()), - caption=caption + caption=caption, + **input_kw ) # Once the media type is properly specified and the file uploaded, From 4fba27dee9a6fdb31b85567f97582bbcd629bb00 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 4 Jan 2018 15:33:48 +0100 Subject: [PATCH 049/361] Accept InputFile/InputFileBig on .upload_file for 2c437c51 Now an input file thumbnail can also be specified, instead needing to reupload the file every time. --- telethon/telegram_bare_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 036aafd2..28bd3334 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -624,6 +624,9 @@ class TelegramBareClient: part_size_kb = get_appropriated_part_size(file_size) file_name = os.path.basename(file_path) """ + if isinstance(file, (InputFile, InputFileBig)): + return file # Already uploaded + if isinstance(file, str): file_size = os.path.getsize(file) elif isinstance(file, bytes): From b45b63d71d06b2b011f9534d523c3c2bd949b8ec Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 4 Jan 2018 21:07:29 +0100 Subject: [PATCH 050/361] Assert ._first_request is True with None auth_key (#517) --- telethon/telegram_bare_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 28bd3334..8a0c43d9 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -507,6 +507,7 @@ class TelegramBareClient: if not self.session.auth_key: __log__.info('Need to generate new auth key before invoking') + self._first_request = True self.session.auth_key, self.session.time_offset = \ authenticator.do_authentication(self._sender.connection) From 6cb5931e7a71467cc6e48c9f6f947bb0b6279312 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 4 Jan 2018 23:37:47 +0100 Subject: [PATCH 051/361] Call .disconnect() on client.__del__ for convenience --- telethon/telegram_bare_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 8a0c43d9..93453322 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -270,6 +270,9 @@ class TelegramBareClient: # TODO Shall we clear the _exported_sessions, or may be reused? self._first_request = True # On reconnect it will be first again + def __del__(self): + self.disconnect() + def _reconnect(self, new_dc=None): """If 'new_dc' is not set, only a call to .connect() will be made since it's assumed that the connection has been lost and the From cb45e8fca991b13e7e81cbef524ef0b34e5a4ed8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 00:59:53 +0100 Subject: [PATCH 052/361] Clean up and complete RTD documentation --- .../accessing-the-full-api.rst | 71 ++-- readthedocs/extra/advanced-usage/sessions.rst | 46 +++ .../extra/advanced-usage/signing-in.rst | 58 ---- .../extra/advanced-usage/users-and-chats.rst | 324 ------------------ readthedocs/extra/advanced.rst | 48 --- readthedocs/extra/basic/creating-a-client.rst | 111 +++++- readthedocs/extra/basic/entities.rst | 87 +++++ readthedocs/extra/basic/getting-started.rst | 17 +- readthedocs/extra/basic/installation.rst | 39 +-- readthedocs/extra/basic/sending-requests.rst | 55 --- readthedocs/extra/basic/sessions.rst | 48 --- readthedocs/extra/basic/telegram-client.rst | 99 ++++++ .../extra/basic/working-with-updates.rst | 65 ++-- readthedocs/extra/developing/api-status.rst | 54 +++ readthedocs/extra/developing/coding-style.rst | 22 ++ readthedocs/extra/developing/philosophy.rst | 25 ++ .../extra/developing/project-structure.rst | 43 +++ .../telegram-api-in-other-languages.rst | 64 ++++ readthedocs/extra/developing/test-servers.rst | 32 ++ .../tips-for-porting-the-project.rst | 17 + .../understanding-the-type-language.rst | 35 ++ .../{advanced-usage => examples}/bots.rst | 22 +- .../extra/examples/chats-and-channels.rst | 205 +++++++++++ .../working-with-messages.rst | 57 +-- ...eleted-limited-or-deactivated-accounts.rst | 6 +- .../extra/troubleshooting/enable-logging.rst | 28 +- .../extra/troubleshooting/rpc-errors.rst | 10 +- readthedocs/extra/wall-of-shame.rst | 57 +++ readthedocs/index.rst | 53 ++- 29 files changed, 1096 insertions(+), 702 deletions(-) rename readthedocs/extra/{basic => advanced-usage}/accessing-the-full-api.rst (58%) create mode 100644 readthedocs/extra/advanced-usage/sessions.rst delete mode 100644 readthedocs/extra/advanced-usage/signing-in.rst delete mode 100644 readthedocs/extra/advanced-usage/users-and-chats.rst delete mode 100644 readthedocs/extra/advanced.rst create mode 100644 readthedocs/extra/basic/entities.rst delete mode 100644 readthedocs/extra/basic/sending-requests.rst delete mode 100644 readthedocs/extra/basic/sessions.rst create mode 100644 readthedocs/extra/basic/telegram-client.rst create mode 100644 readthedocs/extra/developing/api-status.rst create mode 100644 readthedocs/extra/developing/coding-style.rst create mode 100644 readthedocs/extra/developing/philosophy.rst create mode 100644 readthedocs/extra/developing/project-structure.rst create mode 100644 readthedocs/extra/developing/telegram-api-in-other-languages.rst create mode 100644 readthedocs/extra/developing/test-servers.rst create mode 100644 readthedocs/extra/developing/tips-for-porting-the-project.rst create mode 100644 readthedocs/extra/developing/understanding-the-type-language.rst rename readthedocs/extra/{advanced-usage => examples}/bots.rst (77%) create mode 100644 readthedocs/extra/examples/chats-and-channels.rst rename readthedocs/extra/{advanced-usage => examples}/working-with-messages.rst (65%) create mode 100644 readthedocs/extra/wall-of-shame.rst diff --git a/readthedocs/extra/basic/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst similarity index 58% rename from readthedocs/extra/basic/accessing-the-full-api.rst rename to readthedocs/extra/advanced-usage/accessing-the-full-api.rst index ab6682db..04659bdb 100644 --- a/readthedocs/extra/basic/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -1,33 +1,41 @@ .. _accessing-the-full-api: -========================== +====================== Accessing the Full API -========================== +====================== -The ``TelegramClient`` doesn’t offer a method for every single request -the Telegram API supports. However, it’s very simple to ``.invoke()`` -any request. Whenever you need something, don’t forget to `check the + +The ``TelegramClient`` doesn't offer a method for every single request +the Telegram API supports. However, it's very simple to *call* or *invoke* +any request. Whenever you need something, don't forget to `check the documentation`__ and look for the `method you need`__. There you can go through a sorted list of everything you can do. + +.. note:: + + Removing the hand crafted documentation for methods is still + a work in progress! + + You should also refer to the documentation to see what the objects (constructors) Telegram returns look like. Every constructor inherits -from a common type, and that’s the reason for this distinction. +from a common type, and that's the reason for this distinction. -Say ``client.send_message()`` didn’t exist, we could use the `search`__ -to look for “message”. There we would find `SendMessageRequest`__, +Say ``client.send_message()`` didn't exist, we could use the `search`__ +to look for "message". There we would find `SendMessageRequest`__, which we can work with. Every request is a Python class, and has the parameters needed for you to invoke it. You can also call ``help(request)`` for information on -what input parameters it takes. Remember to “Copy import to the -clipboard”, or your script won’t be aware of this class! Now we have: +what input parameters it takes. Remember to "Copy import to the +clipboard", or your script won't be aware of this class! Now we have: .. code-block:: python from telethon.tl.functions.messages import SendMessageRequest -If you’re going to use a lot of these, you may do: +If you're going to use a lot of these, you may do: .. code-block:: python @@ -53,20 +61,20 @@ Or we call ``.get_input_entity()``: peer = client.get_input_entity('someone') -When you’re going to invoke an API method, most require you to pass an +When you're going to invoke an API method, most require you to pass an ``InputUser``, ``InputChat``, or so on, this is why using -``.get_input_entity()`` is more straightforward (and sometimes -immediate, if you know the ID of the user for instance). If you also -need to have information about the whole user, use ``.get_entity()`` -instead: +``.get_input_entity()`` is more straightforward (and often +immediate, if you've seen the user before, know their ID, etc.). +If you also need to have information about the whole user, use +``.get_entity()`` instead: .. code-block:: python entity = client.get_entity('someone') In the later case, when you use the entity, the library will cast it to -its “input” version for you. If you already have the complete user and -want to cache its input version so the library doesn’t have to do this +its "input" version for you. If you already have the complete user and +want to cache its input version so the library doesn't have to do this every time its used, simply call ``.get_input_peer``: .. code-block:: python @@ -83,10 +91,9 @@ request we do: result = client(SendMessageRequest(peer, 'Hello there!')) # __call__ is an alias for client.invoke(request). Both will work -Message sent! Of course, this is only an example. -There are nearly 250 methods available as of layer 73, -and you can use every single of them as you wish. -Remember to use the right types! To sum up: +Message sent! Of course, this is only an example. There are nearly 250 +methods available as of layer 73, and you can use every single of them +as you wish. Remember to use the right types! To sum up: .. code-block:: python @@ -97,16 +104,16 @@ Remember to use the right types! To sum up: .. note:: - Note that some requests have a "hash" parameter. This is **not** your ``api_hash``! - It likely isn't your self-user ``.access_hash`` either. - It's a special hash used by Telegram to only send a difference of new data - that you don't already have with that request, - so you can leave it to 0, and it should work (which means no hash is known yet). + Note that some requests have a "hash" parameter. This is **not** + your ``api_hash``! It likely isn't your self-user ``.access_hash`` either. - For those requests having a "limit" parameter, - you can often set it to zero to signify "return as many items as possible". - This won't work for all of them though, - for instance, in "messages.search" it will actually return 0 items. + It's a special hash used by Telegram to only send a difference of new data + that you don't already have with that request, so you can leave it to 0, + and it should work (which means no hash is known yet). + + For those requests having a "limit" parameter, you can often set it to + zero to signify "return default amount". This won't work for all of them + though, for instance, in "messages.search" it will actually return 0 items. __ https://lonamiwebs.github.io/Telethon @@ -114,4 +121,4 @@ __ https://lonamiwebs.github.io/Telethon/methods/index.html __ https://lonamiwebs.github.io/Telethon/?q=message __ https://lonamiwebs.github.io/Telethon/methods/messages/send_message.html __ https://lonamiwebs.github.io/Telethon/types/input_peer.html -__ https://lonamiwebs.github.io/Telethon/constructors/input_peer_user.html \ No newline at end of file +__ https://lonamiwebs.github.io/Telethon/constructors/input_peer_user.html diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst new file mode 100644 index 00000000..7f1ded9b --- /dev/null +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -0,0 +1,46 @@ +.. _sessions: + +============== +Session Files +============== + +The first parameter you pass the the constructor of the ``TelegramClient`` is +the ``session``, and defaults to be the session name (or full path). That is, +if you create a ``TelegramClient('anon')`` instance and connect, an +``anon.session`` file will be created on the working directory. + +These database files using ``sqlite3`` contain the required information to +talk to the Telegram servers, such as to which IP the client should connect, +port, authorization key so that messages can be encrypted, and so on. + +These files will by default also save all the input entities that you've seen, +so that you can get information about an user or channel by just their ID. +Telegram will **not** send their ``access_hash`` required to retrieve more +information about them, if it thinks you have already seem them. For this +reason, the library needs to store this information offline. + +The library will by default too save all the entities (chats and channels +with their name and username, and users with the phone too) in the session +file, so that you can quickly access them by username or phone number. + +If you're not going to work with updates, or don't need to cache the +``access_hash`` associated with the entities' ID, you can disable this +by setting ``client.session.save_entities = False``, or pass it as a +parameter to the ``TelegramClient``. + +If you don't want to save the files as a database, you can also create +your custom ``Session`` subclass and override the ``.save()`` and ``.load()`` +methods. For example, you could save it on a database: + + .. code-block:: python + + class DatabaseSession(Session): + def save(): + # serialize relevant data to the database + + def load(): + # load relevant data to the database + + +You should read the ````session.py```` source file to know what "relevant +data" you need to keep track of. diff --git a/readthedocs/extra/advanced-usage/signing-in.rst b/readthedocs/extra/advanced-usage/signing-in.rst deleted file mode 100644 index 08f4fe3d..00000000 --- a/readthedocs/extra/advanced-usage/signing-in.rst +++ /dev/null @@ -1,58 +0,0 @@ -========================= -Signing In -========================= - -.. note:: - Make sure you have gone through :ref:`prelude` already! - - -Two Factor Authorization (2FA) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you have Two Factor Authorization (from now on, 2FA) enabled on your account, calling -:meth:`telethon.TelegramClient.sign_in` will raise a `SessionPasswordNeededError`. -When this happens, just :meth:`telethon.TelegramClient.sign_in` again with a ``password=``: - - .. code-block:: python - - import getpass - from telethon.errors import SessionPasswordNeededError - - client.sign_in(phone) - try: - client.sign_in(code=input('Enter code: ')) - except SessionPasswordNeededError: - client.sign_in(password=getpass.getpass()) - -Enabling 2FA -************* - -If you don't have 2FA enabled, but you would like to do so through Telethon, take as example the following code snippet: - - .. code-block:: python - - import os - from hashlib import sha256 - from telethon.tl.functions import account - from telethon.tl.types.account import PasswordInputSettings - - new_salt = client(account.GetPasswordRequest()).new_salt - salt = new_salt + os.urandom(8) # new random salt - - pw = 'secret'.encode('utf-8') # type your new password here - hint = 'hint' - - pw_salted = salt + pw + salt - pw_hash = sha256(pw_salted).digest() - - result = client(account.UpdatePasswordSettingsRequest( - current_password_hash=salt, - new_settings=PasswordInputSettings( - new_salt=salt, - new_password_hash=pw_hash, - hint=hint - ) - )) - -Thanks to `Issue 259 `_ for the tip! - diff --git a/readthedocs/extra/advanced-usage/users-and-chats.rst b/readthedocs/extra/advanced-usage/users-and-chats.rst deleted file mode 100644 index a48a2857..00000000 --- a/readthedocs/extra/advanced-usage/users-and-chats.rst +++ /dev/null @@ -1,324 +0,0 @@ -========================= -Users and Chats -========================= - -.. note:: - Make sure you have gone through :ref:`prelude` already! - -.. contents:: - :depth: 2 - -.. _retrieving-an-entity: - -Retrieving an entity (user or group) -************************************** -An “entity” is used to refer to either an `User`__ or a `Chat`__ -(which includes a `Channel`__). The most straightforward way to get -an entity is to use ``TelegramClient.get_entity()``. This method accepts -either a string, which can be a username, phone number or `t.me`__-like -link, or an integer that will be the ID of an **user**. You can use it -like so: - - .. code-block:: python - - # all of these work - lonami = client.get_entity('lonami') - lonami = client.get_entity('t.me/lonami') - lonami = client.get_entity('https://telegram.dog/lonami') - - # other kind of entities - channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') - contact = client.get_entity('+34xxxxxxxxx') - friend = client.get_entity(friend_id) - -For the last one to work, the library must have “seen” the user at least -once. The library will “see” the user as long as any request contains -them, so if you’ve called ``.get_dialogs()`` for instance, and your -friend was there, the library will know about them. For more, read about -the :ref:`sessions`. - -If you want to get a channel or chat by ID, you need to specify that -they are a channel or a chat. The library can’t infer what they are by -just their ID (unless the ID is marked, but this is only done -internally), so you need to wrap the ID around a `Peer`__ object: - - .. code-block:: python - - from telethon.tl.types import PeerUser, PeerChat, PeerChannel - my_user = client.get_entity(PeerUser(some_id)) - my_chat = client.get_entity(PeerChat(some_id)) - my_channel = client.get_entity(PeerChannel(some_id)) - -**Note** that most requests don’t ask for an ``User``, or a ``Chat``, -but rather for ``InputUser``, ``InputChat``, and so on. If this is the -case, you should prefer ``.get_input_entity()`` over ``.get_entity()``, -as it will be immediate if you provide an ID (whereas ``.get_entity()`` -may need to find who the entity is first). - -Via your open “chats” (dialogs) -------------------------------- - -.. note:: - Please read here: :ref:`retrieving-all-dialogs`. - -Via ResolveUsernameRequest --------------------------- - -This is the request used by ``.get_entity`` internally, but you can also -use it by hand: - -.. code-block:: python - - from telethon.tl.functions.contacts import ResolveUsernameRequest - - result = client(ResolveUsernameRequest('username')) - found_chats = result.chats - found_users = result.users - # result.peer may be a PeerUser, PeerChat or PeerChannel - -See `Peer`__ for more information about this result. - -Via MessageFwdHeader --------------------- - -If all you have is a `MessageFwdHeader`__ after you retrieved a bunch -of messages, this gives you access to the ``from_id`` (if forwarded from -an user) and ``channel_id`` (if forwarded from a channel). Invoking -`GetMessagesRequest`__ also returns a list of ``chats`` and -``users``, and you can find the desired entity there: - - .. code-block:: python - - # Logic to retrieve messages with `GetMessagesRequest´ - messages = foo() - fwd_header = bar() - - user = next(u for u in messages.users if u.id == fwd_header.from_id) - channel = next(c for c in messages.chats if c.id == fwd_header.channel_id) - -Or you can just call ``.get_entity()`` with the ID, as you should have -seen that user or channel before. A call to ``GetMessagesRequest`` may -still be neeed. - -Via GetContactsRequest ----------------------- - -The library will call this for you if you pass a phone number to -``.get_entity``, but again, it can be done manually. If the user you -want to talk to is a contact, you can use `GetContactsRequest`__: - - .. code-block:: python - - from telethon.tl.functions.contacts import GetContactsRequest - from telethon.tl.types.contacts import Contacts - - contacts = client(GetContactsRequest(0)) - if isinstance(contacts, Contacts): - users = contacts.users - contacts = contacts.contacts - -__ https://lonamiwebs.github.io/Telethon/types/user.html -__ https://lonamiwebs.github.io/Telethon/types/chat.html -__ https://lonamiwebs.github.io/Telethon/constructors/channel.html -__ https://t.me -__ https://lonamiwebs.github.io/Telethon/types/peer.html -__ https://lonamiwebs.github.io/Telethon/types/peer.html -__ https://lonamiwebs.github.io/Telethon/constructors/message_fwd_header.html -__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages.html -__ https://lonamiwebs.github.io/Telethon/methods/contacts/get_contacts.html - - -.. _retrieving-all-dialogs: - -Retrieving all dialogs -*********************** - -There are several ``offset_xyz=`` parameters that have no effect at all, -but there's not much one can do since this is something the server should handle. -Currently, the only way to get all dialogs -(open chats, conversations, etc.) is by using the ``offset_date``: - - .. code-block:: python - - from telethon.tl.functions.messages import GetDialogsRequest - from telethon.tl.types import InputPeerEmpty - from time import sleep - - dialogs = [] - users = [] - chats = [] - - last_date = None - chunk_size = 20 - while True: - result = client(GetDialogsRequest( - offset_date=last_date, - offset_id=0, - offset_peer=InputPeerEmpty(), - limit=chunk_size - )) - dialogs.extend(result.dialogs) - users.extend(result.users) - chats.extend(result.chats) - if not result.messages: - break - last_date = min(msg.date for msg in result.messages) - sleep(2) - - -Joining a chat or channel -******************************* - -Note that `Chat`__\ s are normal groups, and `Channel`__\ s are a -special form of `Chat`__\ s, -which can also be super-groups if their ``megagroup`` member is -``True``. - -Joining a public channel ------------------------- - -Once you have the :ref:`entity ` -of the channel you want to join to, you can -make use of the `JoinChannelRequest`__ to join such channel: - - .. code-block:: python - - from telethon.tl.functions.channels import JoinChannelRequest - client(JoinChannelRequest(channel)) - - # In the same way, you can also leave such channel - from telethon.tl.functions.channels import LeaveChannelRequest - client(LeaveChannelRequest(input_channel)) - -For more on channels, check the `channels namespace`__. - -Joining a private chat or channel ---------------------------------- - -If all you have is a link like this one: -``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have -enough information to join! The part after the -``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this -example, is the ``hash`` of the chat or channel. Now you can use -`ImportChatInviteRequest`__ as follows: - - .. -block:: python - - from telethon.tl.functions.messages import ImportChatInviteRequest - updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) - -Adding someone else to such chat or channel -------------------------------------------- - -If you don’t want to add yourself, maybe because you’re already in, you -can always add someone else with the `AddChatUserRequest`__, which -use is very straightforward: - - .. code-block:: python - - from telethon.tl.functions.messages import AddChatUserRequest - - client(AddChatUserRequest( - chat_id, - user_to_add, - fwd_limit=10 # allow the user to see the 10 last messages - )) - -Checking a link without joining -------------------------------- - -If you don’t need to join but rather check whether it’s a group or a -channel, you can use the `CheckChatInviteRequest`__, which takes in -the `hash`__ of said channel or group. - -__ https://lonamiwebs.github.io/Telethon/constructors/chat.html -__ https://lonamiwebs.github.io/Telethon/constructors/channel.html -__ https://lonamiwebs.github.io/Telethon/types/chat.html -__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html -__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html -__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html -__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html -__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html -__ https://github.com/LonamiWebs/Telethon/wiki/Joining-a-chat-or-channel#joining-a-private-chat-or-channel - - -Retrieving all chat members (channels too) -****************************************** - -In order to get all the members from a mega-group or channel, you need -to use `GetParticipantsRequest`__. As we can see it needs an -`InputChannel`__, (passing the mega-group or channel you’re going to -use will work), and a mandatory `ChannelParticipantsFilter`__. The -closest thing to “no filter” is to simply use -`ChannelParticipantsSearch`__ with an empty ``'q'`` string. - -If we want to get *all* the members, we need to use a moving offset and -a fixed limit: - - .. code-block:: python - - from telethon.tl.functions.channels import GetParticipantsRequest - from telethon.tl.types import ChannelParticipantsSearch - from time import sleep - - offset = 0 - limit = 100 - all_participants = [] - - while True: - participants = client.invoke(GetParticipantsRequest( - channel, ChannelParticipantsSearch(''), offset, limit - )) - if not participants.users: - break - all_participants.extend(participants.users) - offset += len(participants.users) - # sleep(1) # This line seems to be optional, no guarantees! - -Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__, -which may have more information you need (like the role of the -participants, total count of members, etc.) - -__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html -__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html -__ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html -__ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html -__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html - - -Recent Actions -******************** - -“Recent actions” is simply the name official applications have given to -the “admin log”. Simply use `GetAdminLogRequest`__ for that, and -you’ll get AdminLogResults.events in return which in turn has the final -`.action`__. - -__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html -__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html - - -Increasing View Count in a Channel -**************************************** - -It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and -while I don’t understand why so many people ask this, the solution is to -use `GetMessagesViewsRequest`__, setting ``increment=True``: - - .. code-block:: python - - - # Obtain `channel' through dialogs or through client.get_entity() or anyhow. - # Obtain `msg_ids' through `.get_message_history()` or anyhow. Must be a list. - - client(GetMessagesViewsRequest( - peer=channel, - id=msg_ids, - increment=True - )) - -__ https://github.com/LonamiWebs/Telethon/issues/233 -__ https://github.com/LonamiWebs/Telethon/issues/305 -__ https://github.com/LonamiWebs/Telethon/issues/409 -__ https://github.com/LonamiWebs/Telethon/issues/447 -__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html \ No newline at end of file diff --git a/readthedocs/extra/advanced.rst b/readthedocs/extra/advanced.rst deleted file mode 100644 index 4433116d..00000000 --- a/readthedocs/extra/advanced.rst +++ /dev/null @@ -1,48 +0,0 @@ -.. _prelude: - -Prelude ---------- - -Before reading any specific example, make sure to read the following common steps: - -All the examples assume that you have successfully created a client and you're authorized as follows: - - .. code-block:: python - - from telethon import TelegramClient - - # Use your own values here - api_id = 12345 - api_hash = '0123456789abcdef0123456789abcdef' - phone_number = '+34600000000' - - client = TelegramClient('some_name', api_id, api_hash) - client.connect() # Must return True, otherwise, try again - - if not client.is_user_authorized(): - client.send_code_request(phone_number) - # .sign_in() may raise PhoneNumberUnoccupiedError - # In that case, you need to call .sign_up() to get a new account - client.sign_in(phone_number, input('Enter code: ')) - - # The `client´ is now ready - -Although Python will probably clean up the resources used by the ``TelegramClient``, -you should always ``.disconnect()`` it once you're done: - - .. code-block:: python - - try: - # Code using the client goes here - except: - # No matter what happens, always disconnect in the end - client.disconnect() - -If the examples aren't enough, you're strongly advised to read the source code -for the InteractiveTelegramClient_ for an overview on how you could build your next script. -This example shows a basic usage more than enough in most cases. Even reading the source -for the TelegramClient_ may help a lot! - - -.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py -.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 997386db..58f36125 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -1,24 +1,28 @@ .. _creating-a-client: -=================== +================= Creating a Client -=================== +================= + Before working with Telegram's API, you need to get your own API ID and hash: -1. Follow `this link `_ and login with your phone number. +1. Follow `this link `_ and login with your + phone number. 2. Click under API Development tools. -3. A *Create new application* window will appear. Fill in your application details. -There is no need to enter any *URL*, and only the first two fields (*App title* and *Short name*) -can be changed later as far as I'm aware. +3. A *Create new application* window will appear. Fill in your application + details. There is no need to enter any *URL*, and only the first two + fields (*App title* and *Short name*) can currently be changed later. -4. Click on *Create application* at the end. Remember that your **API hash is secret** -and Telegram won't let you revoke it. Don't post it anywhere! +4. Click on *Create application* at the end. Remember that your + **API hash is secret** and Telegram won't let you revoke it. + Don't post it anywhere! Once that's ready, the next step is to create a ``TelegramClient``. -This class will be your main interface with Telegram's API, and creating one is very simple: +This class will be your main interface with Telegram's API, and creating +one is very simple: .. code-block:: python @@ -31,14 +35,18 @@ This class will be your main interface with Telegram's API, and creating one is client = TelegramClient('some_name', api_id, api_hash) -Note that ``'some_name'`` will be used to save your session (persistent information such as access key and others) -as ``'some_name.session'`` in your disk. This is simply a JSON file which you can (but shouldn't) modify. -Before using the client, you must be connected to Telegram. Doing so is very easy: +Note that ``'some_name'`` will be used to save your session (persistent +information such as access key and others) as ``'some_name.session'`` in +your disk. This is by default a database file using Python's ``sqlite3``. + +Before using the client, you must be connected to Telegram. +Doing so is very easy: ``client.connect() # Must return True, otherwise, try again`` -You may or may not be authorized yet. You must be authorized before you're able to send any request: +You may or may not be authorized yet. You must be authorized +before you're able to send any request: ``client.is_user_authorized() # Returns True if you can send requests`` @@ -52,13 +60,25 @@ If you're not authorized, you need to ``.sign_in()``: # If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...) # You can import both exceptions from telethon.errors. -``myself`` is your Telegram user. -You can view all the information about yourself by doing ``print(myself.stringify())``. -You're now ready to use the client as you wish! +``myself`` is your Telegram user. You can view all the information about +yourself by doing ``print(myself.stringify())``. You're now ready to use +the client as you wish! Remember that any object returned by the API has +mentioned ``.stringify()`` method, and printing these might prove useful. + +As a full example: + + .. code-block:: python + + client = TelegramClient('anon', api_id, api_hash) + assert client.connect() + if not client.is_user_authorized(): + client.send_code_request(phone_number) + me = client.sign_in(phone_number, input('Enter code: ')) + .. note:: - If you want to use a **proxy**, you have to `install PySocks`__ (via pip or manual) - and then set the appropriated parameters: + If you want to use a **proxy**, you have to `install PySocks`__ + (via pip or manual) and then set the appropriated parameters: .. code-block:: python @@ -72,5 +92,58 @@ You're now ready to use the client as you wish! consisting of parameters described `here`__. + +Two Factor Authorization (2FA) +****************************** + +If you have Two Factor Authorization (from now on, 2FA) enabled on your +account, calling :meth:`telethon.TelegramClient.sign_in` will raise a +`SessionPasswordNeededError`. When this happens, just +:meth:`telethon.TelegramClient.sign_in` again with a ``password=``: + + .. code-block:: python + + import getpass + from telethon.errors import SessionPasswordNeededError + + client.sign_in(phone) + try: + client.sign_in(code=input('Enter code: ')) + except SessionPasswordNeededError: + client.sign_in(password=getpass.getpass()) + + +If you don't have 2FA enabled, but you would like to do so through Telethon, +take as example the following code snippet: + + .. code-block:: python + + import os + from hashlib import sha256 + from telethon.tl.functions import account + from telethon.tl.types.account import PasswordInputSettings + + new_salt = client(account.GetPasswordRequest()).new_salt + salt = new_salt + os.urandom(8) # new random salt + + pw = 'secret'.encode('utf-8') # type your new password here + hint = 'hint' + + pw_salted = salt + pw + salt + pw_hash = sha256(pw_salted).digest() + + result = client(account.UpdatePasswordSettingsRequest( + current_password_hash=salt, + new_settings=PasswordInputSettings( + new_salt=salt, + new_password_hash=pw_hash, + hint=hint + ) + )) + +Thanks to `Issue 259 `_ +for the tip! + + __ https://github.com/Anorov/PySocks#installation -__ https://github.com/Anorov/PySocks#usage-1%3E \ No newline at end of file +__ https://github.com/Anorov/PySocks#usage-1%3E diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst new file mode 100644 index 00000000..c03ec6ce --- /dev/null +++ b/readthedocs/extra/basic/entities.rst @@ -0,0 +1,87 @@ +========================= +Users, Chats and Channels +========================= + + +Introduction +************ + +The library widely uses the concept of "entities". An entity will refer +to any ``User``, ``Chat`` or ``Channel`` object that the API may return +in response to certain methods, such as ``GetUsersRequest``. + +To save bandwidth, the API also makes use of their "input" versions. +The input version of an entity (e.g. ``InputPeerUser``, ``InputChat``, +etc.) only contains the minimum required information that's required +for Telegram to be able to identify who you're referring to: their ID +and hash. This ID/hash pair is unique per user, so if you use the pair +given by another user **or bot** it will **not** work. + +To save *even more* bandwidth, the API also makes use of the ``Peer`` +versions, which just have an ID. This serves to identify them, but +peers alone are not enough to use them. You need to know their hash +before you can "use them". + +Luckily, the library tries to simplify this mess the best it can. + + +Getting entities +**************** + +Through the use of the :ref:`sessions`, the library will automatically +remember the ID and hash pair, along with some extra information, so +you're able to just do this: + + .. code-block:: python + + # dialogs are the "conversations you have open" + # this method returns a list of Dialog, which + # have the .entity attribute and other information. + dialogs = client.get_dialogs(limit=200) + + # all of these work and do the same + lonami = client.get_entity('lonami') + lonami = client.get_entity('t.me/lonami') + lonami = client.get_entity('https://telegram.dog/lonami') + + # other kind of entities + channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') + contact = client.get_entity('+34xxxxxxxxx') + friend = client.get_entity(friend_id) + + # using peers/input peers (note that the API may return these) + # users, chats and channels may all have the same ID, so it's + # necessary to wrap (at least) chat and channels inside Peer. + from telethon.tl.types import PeerUser, PeerChat, PeerChannel + my_user = client.get_entity(PeerUser(some_id)) + my_chat = client.get_entity(PeerChat(some_id)) + my_channel = client.get_entity(PeerChannel(some_id)) + + +All methods in the :ref:`telegram-client` call ``.get_entity()`` to further +save you from the hassle of doing so manually, so doing things like +``client.send_message('lonami', 'hi!')`` is possible. + +Every entity the library "sees" (in any response to any call) will by +default be cached in the ``.session`` file, to avoid performing +unnecessary API calls. If the entity cannot be found, some calls +like ``ResolveUsernameRequest`` or ``GetContactsRequest`` may be +made to obtain the required information. + + +Entities vs. Input Entities +*************************** + +As we mentioned before, API calls don't need to know the whole information +about the entities, only their ID and hash. For this reason, another method, +``.get_input_entity()`` is available. This will always use the cache while +possible, making zero API calls most of the time. When a request is made, +if you provided the full entity, e.g. an ``User``, the library will convert +it to the required ``InputPeer`` automatically for you. + +**You should always favour ``.get_input_entity()``** over ``.get_entity()`` +for this reason! Calling the latter will always make an API call to get +the most recent information about said entity, but invoking requests don't +need this information, just the ``InputPeer``. Only use ``.get_entity()`` +if you need to get actual information, like the username, name, title, etc. +of the entity. diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index bad3ea30..de0b3baf 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -3,13 +3,13 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +=============== +Getting Started +=============== -================= -Getting Started! -================= Simple Installation -********************* +******************* ``pip install telethon`` @@ -17,7 +17,7 @@ Simple Installation Creating a client -************** +***************** .. code-block:: python @@ -39,8 +39,9 @@ Creating a client **More details**: :ref:`creating-a-client` -Simple Stuff -************** +Basic Usage +*********** + .. code-block:: python print(me.stringify()) @@ -52,3 +53,5 @@ Simple Stuff total, messages, senders = client.get_message_history('username') client.download_media(messages[0]) + **More details**: :ref:`telegram-client` + diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index ecad699b..03aed393 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -1,18 +1,20 @@ .. _installation: -================= +============ Installation -================= +============ Automatic Installation -^^^^^^^^^^^^^^^^^^^^^^^ +********************** + To install Telethon, simply do: ``pip install telethon`` -If you get something like ``"SyntaxError: invalid syntax"`` or any other error while installing, -it's probably because ``pip`` defaults to Python 2, which is not supported. Use ``pip3`` instead. +If you get something like ``"SyntaxError: invalid syntax"`` or any other +error while installing/importing the library, it's probably because ``pip`` +defaults to Python 2, which is not supported. Use ``pip3`` instead. If you already have the library installed, upgrade with: @@ -20,7 +22,7 @@ If you already have the library installed, upgrade with: You can also install the library directly from GitHub or a fork: - .. code-block:: python + .. code-block:: sh # pip install git+https://github.com/LonamiWebs/Telethon.git or @@ -32,13 +34,15 @@ If you don't have root access, simply pass the ``--user`` flag to the pip comman Manual Installation -^^^^^^^^^^^^^^^^^^^^ +******************* -1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and ``rsa`` (`GitHub`__ | `PyPi`__) modules: +1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and + ``rsa`` (`GitHub`__ | `PyPi`__) modules: ``sudo -H pip install pyaes rsa`` -2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git`` +2. Clone Telethon's GitHub repository: + ``git clone https://github.com/LonamiWebs/Telethon.git`` 3. Enter the cloned repository: ``cd Telethon`` @@ -50,22 +54,15 @@ To generate the documentation, ``cd docs`` and then ``python3 generate.py``. Optional dependencies -^^^^^^^^^^^^^^^^^^^^^^^^ - -If you're using the library under ARM (or even if you aren't), -you may want to install ``sympy`` through ``pip`` for a substantial speed-up -when generating the keys required to connect to Telegram -(you can of course do this on desktop too). See `issue #199`__ for more. - -If ``libssl`` is available on your system, it will also be used wherever encryption is needed. - -If neither of these are available, a pure Python callback will be used instead, -so you can still run the library wherever Python is available! +********************* +If ``libssl`` is available on your system, it will be used wherever encryption +is needed, but otherwise it will fall back to pure Python implementation so it +will also work without it. __ https://github.com/ricmoo/pyaes __ https://pypi.python.org/pypi/pyaes __ https://github.com/sybrenstuvel/python-rsa/ __ https://pypi.python.org/pypi/rsa/3.4.2 -__ https://github.com/LonamiWebs/Telethon/issues/199 \ No newline at end of file +__ https://github.com/LonamiWebs/Telethon/issues/199 diff --git a/readthedocs/extra/basic/sending-requests.rst b/readthedocs/extra/basic/sending-requests.rst deleted file mode 100644 index 160e2259..00000000 --- a/readthedocs/extra/basic/sending-requests.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. _sending-requests: - -================== -Sending Requests -================== - -Since we're working with Python, one must not forget that they can do ``help(client)`` or ``help(TelegramClient)`` -at any time for a more detailed description and a list of all the available methods. -Calling ``help()`` from an interactive Python session will always list all the methods for any object, even yours! - -Interacting with the Telegram API is done through sending **requests**, -this is, any "method" listed on the API. There are a few methods on the ``TelegramClient`` class -that abstract you from the need of manually importing the requests you need. - -For instance, retrieving your own user can be done in a single line: - - ``myself = client.get_me()`` - -Internally, this method has sent a request to Telegram, who replied with the information about your own user. - -If you want to retrieve any other user, chat or channel (channels are a special subset of chats), -you want to retrieve their "entity". This is how the library refers to either of these: - - .. code-block:: python - - # The method will infer that you've passed an username - # It also accepts phone numbers, and will get the user - # from your contact list. - lonami = client.get_entity('lonami') - -Note that saving and using these entities will be more important when Accessing the Full API. -For now, this is a good way to get information about an user or chat. - -Other common methods for quick scripts are also available: - - .. code-block:: python - - # Sending a message (use an entity/username/etc) - client.send_message('TheAyyBot', 'ayy') - - # Sending a photo, or a file - client.send_file(myself, '/path/to/the/file.jpg', force_document=True) - - # Downloading someone's profile photo. File is saved to 'where' - where = client.download_profile_photo(someone) - - # Retrieving the message history - total, messages, senders = client.get_message_history(someone) - - # Downloading the media from a specific message - # You can specify either a directory, a filename, or nothing at all - where = client.download_media(message, '/path/to/output') - -Remember that you can call ``.stringify()`` to any object Telegram returns to pretty print it. -Calling ``str(result)`` does the same operation, but on a single line. diff --git a/readthedocs/extra/basic/sessions.rst b/readthedocs/extra/basic/sessions.rst deleted file mode 100644 index f55d9703..00000000 --- a/readthedocs/extra/basic/sessions.rst +++ /dev/null @@ -1,48 +0,0 @@ -.. _sessions: - -============== -Session Files -============== - -The first parameter you pass the constructor of the -``TelegramClient`` is the ``session``, and defaults to be the session -name (or full path). That is, if you create a ``TelegramClient('anon')`` -instance and connect, an ``anon.session`` file will be created on the -working directory. - -These JSON session files contain the required information to talk to the -Telegram servers, such as to which IP the client should connect, port, -authorization key so that messages can be encrypted, and so on. - -These files will by default also save all the input entities that you’ve -seen, so that you can get information about an user or channel by just -their ID. Telegram will **not** send their ``access_hash`` required to -retrieve more information about them, if it thinks you have already seem -them. For this reason, the library needs to store this information -offline. - -The library will by default too save all the entities (users with their -name, username, chats and so on) **in memory**, not to disk, so that you -can quickly access them by username or phone number. This can be -disabled too. Run ``help(client.session.entities)`` to see the available -methods (or ``help(EntityDatabase)``). - -If you’re not going to work without updates, or don’t need to cache the -``access_hash`` associated with the entities’ ID, you can disable this -by setting ``client.session.save_entities = False``. - -If you don’t want to save the files as JSON, you can also create your -custom ``Session`` subclass and override the ``.save()`` and ``.load()`` -methods. For example, you could save it on a database: - - .. code-block:: python - - class DatabaseSession(Session): - def save(): - # serialize relevant data to the database - - def load(): - # load relevant data to the database - -You should read the ``session.py`` source file to know what “relevant -data” you need to keep track of. diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst new file mode 100644 index 00000000..5663f533 --- /dev/null +++ b/readthedocs/extra/basic/telegram-client.rst @@ -0,0 +1,99 @@ +.. _telegram-client: + +============== +TelegramClient +============== + + +Introduction +************ + +The ``TelegramClient`` is the central class of the library, the one +you will be using most of the time. For this reason, it's important +to know what it offers. + +Since we're working with Python, one must not forget that we can do +``help(client)`` or ``help(TelegramClient)`` at any time for a more +detailed description and a list of all the available methods. Calling +``help()`` from an interactive Python session will always list all the +methods for any object, even yours! + +Interacting with the Telegram API is done through sending **requests**, +this is, any "method" listed on the API. There are a few methods (and +growing!) on the ``TelegramClient`` class that abstract you from the +need of manually importing the requests you need. + +For instance, retrieving your own user can be done in a single line: + + ``myself = client.get_me()`` + +Internally, this method has sent a request to Telegram, who replied with +the information about your own user, and then the desired information +was extracted from their response. + +If you want to retrieve any other user, chat or channel (channels are a +special subset of chats), you want to retrieve their "entity". This is +how the library refers to either of these: + + .. code-block:: python + + # The method will infer that you've passed an username + # It also accepts phone numbers, and will get the user + # from your contact list. + lonami = client.get_entity('lonami') + +The so called "entities" are another important whole concept on its own, +and you should +Note that saving and using these entities will be more important when +Accessing the Full API. For now, this is a good way to get information +about an user or chat. + +Other common methods for quick scripts are also available: + + .. code-block:: python + + # Sending a message (use an entity/username/etc) + client.send_message('TheAyyBot', 'ayy') + + # Sending a photo, or a file + client.send_file(myself, '/path/to/the/file.jpg', force_document=True) + + # Downloading someone's profile photo. File is saved to 'where' + where = client.download_profile_photo(someone) + + # Retrieving the message history + messages = client.get_message_history(someone) + + # Downloading the media from a specific message + # You can specify either a directory, a filename, or nothing at all + where = client.download_media(message, '/path/to/output') + + # Call .disconnect() when you're done + client.disconnect() + +Remember that you can call ``.stringify()`` to any object Telegram returns +to pretty print it. Calling ``str(result)`` does the same operation, but on +a single line. + + +Available methods +***************** + +This page lists all the "handy" methods available for you to use in the +``TelegramClient`` class. These are simply wrappers around the "raw" +Telegram API, making it much more manageable and easier to work with. + +Please refer to :ref:`accessing-the-full-api` if these aren't enough, +and don't be afraid to read the source code of the InteractiveTelegramClient_ +or even the TelegramClient_ itself to learn how it works. + + +.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py +.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py + + + +.. automodule:: telethon.telegram_client + :members: + :undoc-members: + :show-inheritance: diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index c5d9e919..bb78eb97 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -14,23 +14,24 @@ The library can run in four distinguishable modes: - With several worker threads that run your update handlers. - A mix of the above. -Since this section is about updates, we'll describe the simplest way to work with them. - -.. warning:: - Remember that you should always call ``client.disconnect()`` once you're done. +Since this section is about updates, we'll describe the simplest way to +work with them. Using multiple workers -^^^^^^^^^^^^^^^^^^^^^^^ +********************** -When you create your client, simply pass a number to the ``update_workers`` parameter: +When you create your client, simply pass a number to the +``update_workers`` parameter: ``client = TelegramClient('session', api_id, api_hash, update_workers=4)`` -4 workers should suffice for most cases (this is also the default on `Python Telegram Bot`__). -You can set this value to more, or even less if you need. +4 workers should suffice for most cases (this is also the default on +`Python Telegram Bot`__). You can set this value to more, or even less +if you need. -The next thing you want to do is to add a method that will be called when an `Update`__ arrives: +The next thing you want to do is to add a method that will be called when +an `Update`__ arrives: .. code-block:: python @@ -41,7 +42,8 @@ The next thing you want to do is to add a method that will be called when an `Up # do more work here, or simply sleep! That's it! Now let's do something more interesting. -Every time an user talks to use, let's reply to them with the same text reversed: +Every time an user talks to use, let's reply to them with the same +text reversed: .. code-block:: python @@ -56,16 +58,18 @@ Every time an user talks to use, let's reply to them with the same text reversed input('Press enter to stop this!') client.disconnect() -We only ask you one thing: don't keep this running for too long, or your contacts will go mad. +We only ask you one thing: don't keep this running for too long, or your +contacts will go mad. Spawning no worker at all -^^^^^^^^^^^^^^^^^^^^^^^^^^ +************************* -All the workers do is loop forever and poll updates from a queue that is filled from the ``ReadThread``, -responsible for reading every item off the network. -If you only need a worker and the ``MainThread`` would be doing no other job, -this is the preferred way. You can easily do the same as the workers like so: +All the workers do is loop forever and poll updates from a queue that is +filled from the ``ReadThread``, responsible for reading every item off +the network. If you only need a worker and the ``MainThread`` would be +doing no other job, this is the preferred way. You can easily do the same +as the workers like so: .. code-block:: python @@ -81,24 +85,27 @@ this is the preferred way. You can easily do the same as the workers like so: client.disconnect() -Note that ``poll`` accepts a ``timeout=`` parameter, -and it will return ``None`` if other thread got the update before you could or if the timeout expired, -so it's important to check ``if not update``. +Note that ``poll`` accepts a ``timeout=`` parameter, and it will return +``None`` if other thread got the update before you could or if the timeout +expired, so it's important to check ``if not update``. -This can coexist with the rest of ``N`` workers, or you can set it to ``0`` additional workers: +This can coexist with the rest of ``N`` workers, or you can set it to ``0`` +additional workers: ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` -You **must** set it to ``0`` (or other number), as it defaults to ``None`` and there is a different. -``None`` workers means updates won't be processed *at all*, -so you must set it to some value (0 or greater) if you want ``client.updates.poll()`` to work. +You **must** set it to ``0`` (or other number), as it defaults to ``None`` +and there is a different. ``None`` workers means updates won't be processed +*at all*, so you must set it to some value (``0`` or greater) if you want +``client.updates.poll()`` to work. Using the main thread instead the ``ReadThread`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +************************************************ -If you have no work to do on the ``MainThread`` and you were planning to have a ``while True: sleep(1)``, -don't do that. Instead, don't spawn the secondary ``ReadThread`` at all like so: +If you have no work to do on the ``MainThread`` and you were planning to have +a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary +``ReadThread`` at all like so: .. code-block:: python @@ -111,8 +118,8 @@ And then ``.idle()`` from the ``MainThread``: ``client.idle()`` -You can stop it with :kbd:`Control+C`, -and you can configure the signals to be used in a similar fashion to `Python Telegram Bot`__. +You can stop it with :kbd:`Control+C`, and you can configure the signals +to be used in a similar fashion to `Python Telegram Bot`__. As a complete example: @@ -132,4 +139,4 @@ As a complete example: __ https://python-telegram-bot.org/ __ https://lonamiwebs.github.io/Telethon/types/update.html -__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 \ No newline at end of file +__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 diff --git a/readthedocs/extra/developing/api-status.rst b/readthedocs/extra/developing/api-status.rst new file mode 100644 index 00000000..b5092dad --- /dev/null +++ b/readthedocs/extra/developing/api-status.rst @@ -0,0 +1,54 @@ +========== +API Status +========== + + +In an attempt to help everyone who works with the Telegram API, the +library will by default report all *Remote Procedure Call* errors to +`RPC PWRTelegram `__, a public database +anyone can query, made by `Daniil `__. All the +information sent is a ``GET`` request with the error code, error message +and method used. + +If you still would like to opt out, simply set +``client.session.report_errors = False`` to disable this feature, or +pass ``report_errors=False`` as a named parameter when creating a +``TelegramClient`` instance. However Daniil would really thank you if +you helped him (and everyone) by keeping it on! + +Querying the API status +*********************** + +The API is accessed through ``GET`` requests, which can be made for +instance through ``curl``. A JSON response will be returned. + +**All known errors and their description**: + +.. code:: bash + + curl https://rpc.pwrtelegram.xyz/?all + +**Error codes for a specific request**: + +.. code:: bash + + curl https://rpc.pwrtelegram.xyz/?for=messages.sendMessage + +**Number of ``RPC_CALL_FAIL``\ 's**: + +.. code:: bash + + curl https://rpc.pwrtelegram.xyz/?rip # last hour + curl https://rpc.pwrtelegram.xyz/?rip=$(time()-60) # last minute + +**Description of errors**: + +.. code:: bash + + curl https://rpc.pwrtelegram.xyz/?description_for=SESSION_REVOKED + +**Code of a specific error**: + +.. code:: bash + + curl https://rpc.pwrtelegram.xyz/?code_for=STICKERSET_INVALID diff --git a/readthedocs/extra/developing/coding-style.rst b/readthedocs/extra/developing/coding-style.rst new file mode 100644 index 00000000..c629034c --- /dev/null +++ b/readthedocs/extra/developing/coding-style.rst @@ -0,0 +1,22 @@ +============ +Coding Style +============ + + +Basically, make it **readable**, while keeping the style similar to the +code of whatever file you're working on. + +Also note that not everyone has 4K screens for their primary monitors, +so please try to stick to the 80-columns limit. This makes it easy to +``git diff`` changes from a terminal before committing changes. If the +line has to be long, please don't exceed 120 characters. + +For the commit messages, please make them *explanatory*. Not only +they're helpful to troubleshoot when certain issues could have been +introduced, but they're also used to construct the change log once a new +version is ready. + +If you don't know enough Python, I strongly recommend reading `Dive Into +Python 3 `__, available online for +free. For instance, remember to do ``if x is None`` or +``if x is not None`` instead ``if x == None``! diff --git a/readthedocs/extra/developing/philosophy.rst b/readthedocs/extra/developing/philosophy.rst new file mode 100644 index 00000000..f779be2b --- /dev/null +++ b/readthedocs/extra/developing/philosophy.rst @@ -0,0 +1,25 @@ +========== +Philosophy +========== + + +The intention of the library is to have an existing MTProto library +existing with hardly any dependencies (indeed, wherever Python is +available, you can run this library). + +Being written in Python means that performance will be nowhere close to +other implementations written in, for instance, Java, C++, Rust, or +pretty much any other compiled language. However, the library turns out +to actually be pretty decent for common operations such as sending +messages, receiving updates, or other scripting. Uploading files may be +notably slower, but if you would like to contribute, pull requests are +appreciated! + +If ``libssl`` is available on your system, the library will make use of +it to speed up some critical parts such as encrypting and decrypting the +messages. Files will notably be sent and downloaded faster. + +The main focus is to keep everything clean and simple, for everyone to +understand how working with MTProto and Telegram works. Don't be afraid +to read the source, the code won't bite you! It may prove useful when +using the library on your own use cases. diff --git a/readthedocs/extra/developing/project-structure.rst b/readthedocs/extra/developing/project-structure.rst new file mode 100644 index 00000000..d40c6031 --- /dev/null +++ b/readthedocs/extra/developing/project-structure.rst @@ -0,0 +1,43 @@ +================= +Project Structure +================= + + +Main interface +************** + +The library itself is under the ``telethon/`` directory. The +``__init__.py`` file there exposes the main ``TelegramClient``, a class +that servers as a nice interface with the most commonly used methods on +Telegram such as sending messages, retrieving the message history, +handling updates, etc. + +The ``TelegramClient`` inherits the ``TelegramBareClient``. The later is +basically a pruned version of the ``TelegramClient``, which knows basic +stuff like ``.invoke()``\ 'ing requests, downloading files, or switching +between data centers. This is primary to keep the method count per class +and file low and manageable. + +Both clients make use of the ``network/mtproto_sender.py``. The +``MtProtoSender`` class handles packing requests with the ``salt``, +``id``, ``sequence``, etc., and also handles how to process responses +(i.e. pong, RPC errors). This class communicates through Telegram via +its ``.connection`` member. + +The ``Connection`` class uses a ``extensions/tcp_client``, a C#-like +``TcpClient`` to ease working with sockets in Python. All the +``TcpClient`` know is how to connect through TCP and writing/reading +from the socket with optional cancel. + +The ``Connection`` class bundles up all the connections modes and sends +and receives the messages accordingly (TCP full, obfuscated, +intermediate…). + +Auto-generated code +******************* + +The files under ``telethon_generator/`` are used to generate the code +that gets placed under ``telethon/tl/``. The ``TLGenerator`` takes in a +``.tl`` file, and spits out the generated classes which represent, as +Python classes, the request and types defined in the ``.tl`` file. It +also constructs an index so that they can be imported easily. diff --git a/readthedocs/extra/developing/telegram-api-in-other-languages.rst b/readthedocs/extra/developing/telegram-api-in-other-languages.rst new file mode 100644 index 00000000..0adeb988 --- /dev/null +++ b/readthedocs/extra/developing/telegram-api-in-other-languages.rst @@ -0,0 +1,64 @@ +=============================== +Telegram API in Other Languages +=============================== + + +Telethon was made for **Python**, and as far as I know, there is no +*exact* port to other languages. However, there *are* other +implementations made by awesome people (one needs to be awesome to +understand the official Telegram documentation) on several languages +(even more Python too), listed below: + +C +* + +Possibly the most well-known unofficial open source implementation out +there by `**@vysheng** `__, +```tgl`` `__, and its console client +```telegram-cli`` `__. Latest development +has been moved to `BitBucket `__. + +JavaScript +********** + +`**@zerobias** `__ is working on +```telegram-mtproto`` `__, +a work-in-progress JavaScript library installable via +```npm`` `__. + +Kotlin +****** + +`Kotlogram `__ is a Telegram +implementation written in Kotlin (the now +`official `__ +language for +`Android `__) by +`**@badoualy** `__, currently as a beta– +yet working. + +PHP +*** + +A PHP implementation is also available thanks to +`**@danog** `__ and his +`MadelineProto `__ project, with +a very nice `online +documentation `__ too. + +Python +****** + +A fairly new (as of the end of 2017) Telegram library written from the +ground up in Python by +`**@delivrance** `__ and his +`Pyrogram `__ library! No hard +feelings Dan and good luck dealing with some of your users ;) + +Rust +**** + +Yet another work-in-progress implementation, this time for Rust thanks +to `**@JuanPotato** `__ under the fancy +name of `Vail `__. This one is very +early still, but progress is being made at a steady rate. diff --git a/readthedocs/extra/developing/test-servers.rst b/readthedocs/extra/developing/test-servers.rst new file mode 100644 index 00000000..2ba66897 --- /dev/null +++ b/readthedocs/extra/developing/test-servers.rst @@ -0,0 +1,32 @@ +============ +Test Servers +============ + + +To run Telethon on a test server, use the following code: + + .. code-block:: python + + client = TelegramClient(None, api_id, api_hash) + client.session.server_address = '149.154.167.40' + client.connect() + +You can check your ``'test ip'`` on https://my.telegram.org. + +You should set ``None`` session so to ensure you're generating a new +authorization key for it (it would fail if you used a session where you +had previously connected to another data center). + +Once you're connected, you'll likely need to ``.sign_up()``. Remember +`anyone can access the phone you +choose `__, +so don't store sensitive data here: + + .. code-block:: python + + from random import randint + + dc_id = '2' # Change this to the DC id of the test server you chose + phone = '99966' + dc_id + str(randint(9999)).zfill(4) + client.send_code_request(phone) + client.sign_up(dc_id * 5, 'Some', 'Name') diff --git a/readthedocs/extra/developing/tips-for-porting-the-project.rst b/readthedocs/extra/developing/tips-for-porting-the-project.rst new file mode 100644 index 00000000..c7135096 --- /dev/null +++ b/readthedocs/extra/developing/tips-for-porting-the-project.rst @@ -0,0 +1,17 @@ +============================ +Tips for Porting the Project +============================ + + +If you're going to use the code on this repository to guide you, please +be kind and don't forget to mention it helped you! + +You should start by reading the source code on the `first +release `__ of +the project, and start creating a ``MtProtoSender``. Once this is made, +you should write by hand the code to authenticate on the Telegram's +server, which are some steps required to get the key required to talk to +them. Save it somewhere! Then, simply mimic, or reinvent other parts of +the code, and it will be ready to go within a few days. + +Good luck! diff --git a/readthedocs/extra/developing/understanding-the-type-language.rst b/readthedocs/extra/developing/understanding-the-type-language.rst new file mode 100644 index 00000000..c82063ef --- /dev/null +++ b/readthedocs/extra/developing/understanding-the-type-language.rst @@ -0,0 +1,35 @@ +=============================== +Understanding the Type Language +=============================== + + +`Telegram's Type Language `__ +(also known as TL, found on ``.tl`` files) is a concise way to define +what other programming languages commonly call classes or structs. + +Every definition is written as follows for a Telegram object is defined +as follows: + +.. code:: tl + + name#id argument_name:argument_type = CommonType + +This means that in a single line you know what the ``TLObject`` name is. +You know it's unique ID, and you know what arguments it has. It really +isn't that hard to write a generator for generating code to any +platform! + +The generated code should also be able to *encode* the ``TLObject`` (let +this be a request or a type) into bytes, so they can be sent over the +network. This isn't a big deal either, because you know how the +``TLObject``\ 's are made, and how the types should be serialized. + +You can either write your own code generator, or use the one this +library provides, but please be kind and keep some special mention to +this project for helping you out. + +This is only a introduction. The ``TL`` language is not *that* easy. But +it's not that hard either. You're free to sniff the +``telethon_generator/`` files and learn how to parse other more complex +lines, such as ``flags`` (to indicate things that may or may not be +written at all) and ``vector``\ 's. diff --git a/readthedocs/extra/advanced-usage/bots.rst b/readthedocs/extra/examples/bots.rst similarity index 77% rename from readthedocs/extra/advanced-usage/bots.rst rename to readthedocs/extra/examples/bots.rst index 091eada1..b231e200 100644 --- a/readthedocs/extra/advanced-usage/bots.rst +++ b/readthedocs/extra/examples/bots.rst @@ -1,13 +1,14 @@ -====== +==== Bots -====== +==== + Talking to Inline Bots -^^^^^^^^^^^^^^^^^^^^^^ +********************** -You can query an inline bot, such as `@VoteBot`__ -(note, *query*, not *interact* with a voting message), by making use of -the `GetInlineBotResultsRequest`__ request: +You can query an inline bot, such as `@VoteBot`__ (note, *query*, +not *interact* with a voting message), by making use of the +`GetInlineBotResultsRequest`__ request: .. code-block:: python @@ -32,11 +33,10 @@ And you can select any of their results by using Talking to Bots with special reply markup -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +***************************************** To interact with a message that has a special reply markup, such as -`@VoteBot`__ polls, you would use -`GetBotCallbackAnswerRequest`__: +`@VoteBot`__ polls, you would use `GetBotCallbackAnswerRequest`__: .. code-block:: python @@ -48,7 +48,7 @@ To interact with a message that has a special reply markup, such as data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data )) -It’s a bit verbose, but it has all the information you would need to +It's a bit verbose, but it has all the information you would need to show it visually (button rows, and buttons within each row, each with its own data). @@ -56,4 +56,4 @@ __ https://t.me/vote __ https://lonamiwebs.github.io/Telethon/methods/messages/get_inline_bot_results.html __ https://lonamiwebs.github.io/Telethon/methods/messages/send_inline_bot_result.html __ https://lonamiwebs.github.io/Telethon/methods/messages/get_bot_callback_answer.html -__ https://t.me/vote \ No newline at end of file +__ https://t.me/vote diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst new file mode 100644 index 00000000..1bafec80 --- /dev/null +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -0,0 +1,205 @@ +=============================== +Working with Chats and Channels +=============================== + + +Joining a chat or channel +************************* + +Note that `Chat`__\ s are normal groups, and `Channel`__\ s are a +special form of `Chat`__\ s, +which can also be super-groups if their ``megagroup`` member is +``True``. + + +Joining a public channel +************************ + +Once you have the :ref:`entity ` of the channel you want to join +to, you can make use of the `JoinChannelRequest`__ to join such channel: + + .. code-block:: python + + from telethon.tl.functions.channels import JoinChannelRequest + client(JoinChannelRequest(channel)) + + # In the same way, you can also leave such channel + from telethon.tl.functions.channels import LeaveChannelRequest + client(LeaveChannelRequest(input_channel)) + + +For more on channels, check the `channels namespace`__. + + +Joining a private chat or channel +********************************* + +If all you have is a link like this one: +``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have +enough information to join! The part after the +``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this +example, is the ``hash`` of the chat or channel. Now you can use +`ImportChatInviteRequest`__ as follows: + + .. -block:: python + + from telethon.tl.functions.messages import ImportChatInviteRequest + updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) + + +Adding someone else to such chat or channel +******************************************* + +If you don't want to add yourself, maybe because you're already in, +you can always add someone else with the `AddChatUserRequest`__, +which use is very straightforward: + + .. code-block:: python + + from telethon.tl.functions.messages import AddChatUserRequest + + client(AddChatUserRequest( + chat_id, + user_to_add, + fwd_limit=10 # allow the user to see the 10 last messages + )) + + +Checking a link without joining +******************************* + +If you don't need to join but rather check whether it's a group or a +channel, you can use the `CheckChatInviteRequest`__, which takes in +the `hash`__ of said channel or group. + +__ https://lonamiwebs.github.io/Telethon/constructors/chat.html +__ https://lonamiwebs.github.io/Telethon/constructors/channel.html +__ https://lonamiwebs.github.io/Telethon/types/chat.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html +__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html +__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html +__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html +__ https://github.com/LonamiWebs/Telethon/wiki/Joining-a-chat-or-channel#joining-a-private-chat-or-channel + + +Retrieving all chat members (channels too) +****************************************** + +In order to get all the members from a mega-group or channel, you need +to use `GetParticipantsRequest`__. As we can see it needs an +`InputChannel`__, (passing the mega-group or channel you're going to +use will work), and a mandatory `ChannelParticipantsFilter`__. The +closest thing to "no filter" is to simply use +`ChannelParticipantsSearch`__ with an empty ``'q'`` string. + +If we want to get *all* the members, we need to use a moving offset and +a fixed limit: + + .. code-block:: python + + from telethon.tl.functions.channels import GetParticipantsRequest + from telethon.tl.types import ChannelParticipantsSearch + from time import sleep + + offset = 0 + limit = 100 + all_participants = [] + + while True: + participants = client.invoke(GetParticipantsRequest( + channel, ChannelParticipantsSearch(''), offset, limit + )) + if not participants.users: + break + all_participants.extend(participants.users) + offset += len(participants.users) + + +Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__, +which may have more information you need (like the role of the +participants, total count of members, etc.) + +__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html +__ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html +__ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html +__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html + + +Recent Actions +************** + +"Recent actions" is simply the name official applications have given to +the "admin log". Simply use `GetAdminLogRequest`__ for that, and +you'll get AdminLogResults.events in return which in turn has the final +`.action`__. + +__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html +__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html + + +Admin Permissions +***************** + +Giving or revoking admin permissions can be done with the `EditAdminRequest`__: + + .. code-block:: python + + from telethon.tl.functions.channels import EditAdminRequest + from telethon.tl.types import ChannelAdminRights + + # You need both the channel and who to grant permissions + # They can either be channel/user or input channel/input user. + # + # ChannelAdminRights is a list of granted permissions. + # Set to True those you want to give. + rights = ChannelAdminRights( + post_messages=None, + add_admins=None, + invite_users=None, + change_info=True, + ban_users=None, + delete_messages=True, + pin_messages=True, + invite_link=None, + edit_messages=None + ) + + client(EditAdminRequest(channel, who, rights)) + + +Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set +to ``True`` the ``post_messages`` and ``edit_messages`` fields. Those that +are ``None`` can be omitted (left here so you know `which are available`__. + +__ https://lonamiwebs.github.io/Telethon/methods/channels/edit_admin.html +__ https://github.com/Kyle2142 +__ https://github.com/LonamiWebs/Telethon/issues/490 +__ https://lonamiwebs.github.io/Telethon/constructors/channel_admin_rights.html + + +Increasing View Count in a Channel +********************************** + +It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and +while I don't understand why so many people ask this, the solution is to +use `GetMessagesViewsRequest`__, setting ``increment=True``: + + .. code-block:: python + + + # Obtain `channel' through dialogs or through client.get_entity() or anyhow. + # Obtain `msg_ids' through `.get_message_history()` or anyhow. Must be a list. + + client(GetMessagesViewsRequest( + peer=channel, + id=msg_ids, + increment=True + )) + +__ https://github.com/LonamiWebs/Telethon/issues/233 +__ https://github.com/LonamiWebs/Telethon/issues/305 +__ https://github.com/LonamiWebs/Telethon/issues/409 +__ https://github.com/LonamiWebs/Telethon/issues/447 +__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html diff --git a/readthedocs/extra/advanced-usage/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst similarity index 65% rename from readthedocs/extra/advanced-usage/working-with-messages.rst rename to readthedocs/extra/examples/working-with-messages.rst index 2c141406..880bac6f 100644 --- a/readthedocs/extra/advanced-usage/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -1,20 +1,18 @@ -========================= +===================== Working with messages -========================= - -.. note:: - Make sure you have gone through :ref:`prelude` already! +===================== Forwarding messages ******************* -Note that ForwardMessageRequest_ (note it's Message, singular) will *not* work if channels are involved. -This is because channel (and megagroups) IDs are not unique, so you also need to know who the sender is -(a parameter this request doesn't have). +Note that ForwardMessageRequest_ (note it's Message, singular) will *not* +work if channels are involved. This is because channel (and megagroups) IDs +are not unique, so you also need to know who the sender is (a parameter this +request doesn't have). -Either way, you are encouraged to use ForwardMessagesRequest_ (note it's Message*s*, plural) *always*, -since it is more powerful, as follows: +Either way, you are encouraged to use ForwardMessagesRequest_ (note it's +Message*s*, plural) *always*, since it is more powerful, as follows: .. code-block:: python @@ -31,14 +29,16 @@ since it is more powerful, as follows: to_peer=to_entity # who are we forwarding them to? )) -The named arguments are there for clarity, although they're not needed because they appear in order. -You can obviously just wrap a single message on the list too, if that's all you have. +The named arguments are there for clarity, although they're not needed because +they appear in order. You can obviously just wrap a single message on the list +too, if that's all you have. Searching Messages ******************* -Messages are searched through the obvious SearchRequest_, but you may run into issues_. A valid example would be: +Messages are searched through the obvious SearchRequest_, but you may run +into issues_. A valid example would be: .. code-block:: python @@ -46,27 +46,32 @@ Messages are searched through the obvious SearchRequest_, but you may run into i entity, 'query', InputMessagesFilterEmpty(), None, None, 0, 0, 100 )) -It's important to note that the optional parameter ``from_id`` has been left omitted and thus defaults to ``None``. -Changing it to InputUserEmpty_, as one could think to specify "no user", won't work because this parameter is a flag, +It's important to note that the optional parameter ``from_id`` has been left +omitted and thus defaults to ``None``. Changing it to InputUserEmpty_, as one +could think to specify "no user", won't work because this parameter is a flag, and it being unspecified has a different meaning. -If one were to set ``from_id=InputUserEmpty()``, it would filter messages from "empty" senders, -which would likely match no users. +If one were to set ``from_id=InputUserEmpty()``, it would filter messages +from "empty" senders, which would likely match no users. -If you get a ``ChatAdminRequiredError`` on a channel, it's probably because you tried setting the ``from_id`` filter, -and as the error says, you can't do that. Leave it set to ``None`` and it should work. +If you get a ``ChatAdminRequiredError`` on a channel, it's probably because +you tried setting the ``from_id`` filter, and as the error says, you can't +do that. Leave it set to ``None`` and it should work. -As with every method, make sure you use the right ID/hash combination for your ``InputUser`` or ``InputChat``, -or you'll likely run into errors like ``UserIdInvalidError``. +As with every method, make sure you use the right ID/hash combination for +your ``InputUser`` or ``InputChat``, or you'll likely run into errors like +``UserIdInvalidError``. Sending stickers -***************** +**************** -Stickers are nothing else than ``files``, and when you successfully retrieve the stickers for a certain sticker set, -all you will have are ``handles`` to these files. Remember, the files Telegram holds on their servers can be referenced -through this pair of ID/hash (unique per user), and you need to use this handle when sending a "document" message. -This working example will send yourself the very first sticker you have: +Stickers are nothing else than ``files``, and when you successfully retrieve +the stickers for a certain sticker set, all you will have are ``handles`` to +these files. Remember, the files Telegram holds on their servers can be +referenced through this pair of ID/hash (unique per user), and you need to +use this handle when sending a "document" message. This working example will +send yourself the very first sticker you have: .. code-block:: python diff --git a/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst b/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst index 1ad3da19..6426ada9 100644 --- a/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst +++ b/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst @@ -1,6 +1,6 @@ -========================================= +======================================== Deleted, Limited or Deactivated Accounts -========================================= +======================================== If you're from Iran or Russian, we have bad news for you. Telegram is much more likely to ban these numbers, @@ -23,4 +23,4 @@ For more discussion, please see `issue 297`__. __ https://t.me/SpamBot -__ https://github.com/LonamiWebs/Telethon/issues/297 \ No newline at end of file +__ https://github.com/LonamiWebs/Telethon/issues/297 diff --git a/readthedocs/extra/troubleshooting/enable-logging.rst b/readthedocs/extra/troubleshooting/enable-logging.rst index a6d45d00..897052e2 100644 --- a/readthedocs/extra/troubleshooting/enable-logging.rst +++ b/readthedocs/extra/troubleshooting/enable-logging.rst @@ -1,15 +1,18 @@ ================ -Enable Logging +Enabling Logging ================ Telethon makes use of the `logging`__ module, and you can enable it as follows: - .. code-block:: python +.. code:: python - import logging - logging.basicConfig(level=logging.DEBUG) + import logging + logging.basicConfig(level=logging.DEBUG) -You can also use it in your own project very easily: +The library has the `NullHandler`__ added by default so that no log calls +will be printed unless you explicitly enable it. + +You can also `use the module`__ on your own project very easily: .. code-block:: python @@ -21,4 +24,17 @@ You can also use it in your own project very easily: logger.warning('This is a warning!') -__ https://docs.python.org/3/library/logging.html \ No newline at end of file +If you want to enable ``logging`` for your project *but* use a different +log level for the library: + + .. code-block:: python + + import logging + logging.basicConfig(level=logging.DEBUG) + # For instance, show only warnings and above + logging.getLogger('telethon').setLevel(level=logging.WARNING) + + +__ https://docs.python.org/3/library/logging.html +__ https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library +__ https://docs.python.org/3/howto/logging.html diff --git a/readthedocs/extra/troubleshooting/rpc-errors.rst b/readthedocs/extra/troubleshooting/rpc-errors.rst index 6e8a59f0..3618fb9a 100644 --- a/readthedocs/extra/troubleshooting/rpc-errors.rst +++ b/readthedocs/extra/troubleshooting/rpc-errors.rst @@ -3,9 +3,9 @@ RPC Errors ========== RPC stands for Remote Procedure Call, and when Telethon raises an -``RPCError``, it’s most likely because you have invoked some of the API +``RPCError``, it's most likely because you have invoked some of the API methods incorrectly (wrong parameters, wrong permissions, or even -something went wrong on Telegram’s server). The most common are: +something went wrong on Telegram's server). The most common are: - ``FloodError`` (420), the same request was repeated many times. Must wait ``.seconds``. @@ -13,7 +13,7 @@ something went wrong on Telegram’s server). The most common are: verification on Telegram. - ``CdnFileTamperedError``, if the media you were trying to download from a CDN has been altered. -- ``ChatAdminRequiredError``, you don’t have permissions to perform +- ``ChatAdminRequiredError``, you don't have permissions to perform said operation on a chat or channel. Try avoiding filters, i.e. when searching messages. @@ -22,6 +22,6 @@ The generic classes for different error codes are: \* ``InvalidDCError`` ``BadRequestError`` (400), the request contained errors. \* ``UnauthorizedError`` (401), the user is not authorized yet. \* ``ForbiddenError`` (403), privacy violation error. \* ``NotFoundError`` -(404), make sure you’re invoking ``Request``\ ’s! +(404), make sure you're invoking ``Request``\ 's! -If the error is not recognised, it will only be an ``RPCError``. \ No newline at end of file +If the error is not recognised, it will only be an ``RPCError``. diff --git a/readthedocs/extra/wall-of-shame.rst b/readthedocs/extra/wall-of-shame.rst new file mode 100644 index 00000000..b3c9a028 --- /dev/null +++ b/readthedocs/extra/wall-of-shame.rst @@ -0,0 +1,57 @@ +This project has an +`issues `__ section for +you to file **issues** whenever you encounter any when working with the +library. Said section is **not** for issues on *your* program but rather +issues with Telethon itself. + +If you have not made the effort to 1. `read through the +wiki `__ and 2. `look for +the method you need `__, you +will end up on the `Wall of +Shame `__, +i.e. all issues labeled +`"RTFM" `__: + +> > **rtfm** +> > Literally "Read The F\ **king Manual"; a term showing the +frustration of being bothered with questions so trivial that the asker +could have quickly figured out the answer on their own with minimal +effort, usually by reading readily-available documents. People who +say"RTFM!" might be considered rude, but the true rude ones are the +annoying people who take absolutely no self-responibility and expect to +have all the answers handed to them personally. +> > *"Damn, that's the twelveth time that somebody posted this question +to the messageboard today! RTFM, already!"* +> > **\ by Bill M. July 27, 2004*\* + +If you have indeed read the wiki, and have tried looking for the method, +and yet you didn't find what you need, **that's fine**. Telegram's API +can have some obscure names at times, and for this reason, there is a +`"question" +label `__ +with questions that are okay to ask. Just state what you've tried so +that we know you've made an effort, or you'll go to the Wall of Shame. + +Of course, if the issue you're going to open is not even a question but +a real issue with the library (thankfully, most of the issues have been +that!), you won't end up here. Don't worry. + +Current winner +-------------- + +The current winner is `issue +213 `__: + +**Issue:** + + .. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg + :alt: Winner issue + + Winner issue + +**Answer:** + + .. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg + :alt: Winner issue answer + + Winner issue answer diff --git a/readthedocs/index.rst b/readthedocs/index.rst index b5c77e6b..8e5c6053 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -3,11 +3,14 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +==================================== Welcome to Telethon's documentation! ==================================== -Pure Python 3 Telegram client library. Official Site `here `_. +Pure Python 3 Telegram client library. +Official Site `here `_. +Please follow the links below to get you started. .. _installation-and-usage: @@ -19,10 +22,9 @@ Pure Python 3 Telegram client library. Official Site `here Date: Fri, 5 Jan 2018 01:03:57 +0100 Subject: [PATCH 053/361] Update README to point to RTD instead GitHub's wiki --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c4b9b7e8..f524384e 100755 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ Doing stuff Next steps ---------- -Do you like how Telethon looks? Check the -`wiki over GitHub `_ for a -more in-depth explanation, with examples, troubleshooting issues, and more -useful information. +Do you like how Telethon looks? Check out +`Read The Docs `_ +for a more in-depth explanation, with examples, +troubleshooting issues, and more useful information. From a489b4b18b76c0dccf22b85880505fd5ea54838e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 13:30:21 +0100 Subject: [PATCH 054/361] Clean up some more twirks on RTD and update docstrings --- readthedocs/extra/basic/creating-a-client.rst | 6 +- readthedocs/extra/basic/entities.rst | 14 +- readthedocs/extra/basic/getting-started.rst | 3 +- readthedocs/extra/basic/installation.rst | 1 - readthedocs/extra/developing/api-status.rst | 2 +- .../extra/troubleshooting/rpc-errors.rst | 4 +- readthedocs/extra/wall-of-shame.rst | 5 + telethon/telegram_client.py | 477 +++++++++++------- 8 files changed, 307 insertions(+), 205 deletions(-) diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 58f36125..81e19c83 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -98,7 +98,7 @@ Two Factor Authorization (2FA) If you have Two Factor Authorization (from now on, 2FA) enabled on your account, calling :meth:`telethon.TelegramClient.sign_in` will raise a -`SessionPasswordNeededError`. When this happens, just +``SessionPasswordNeededError``. When this happens, just :meth:`telethon.TelegramClient.sign_in` again with a ``password=``: .. code-block:: python @@ -113,7 +113,7 @@ account, calling :meth:`telethon.TelegramClient.sign_in` will raise a client.sign_in(password=getpass.getpass()) -If you don't have 2FA enabled, but you would like to do so through Telethon, +If you don't have 2FA enabled, but you would like to do so through the library, take as example the following code snippet: .. code-block:: python @@ -146,4 +146,4 @@ for the tip! __ https://github.com/Anorov/PySocks#installation -__ https://github.com/Anorov/PySocks#usage-1%3E +__ https://github.com/Anorov/PySocks#usage-1 diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index c03ec6ce..bc87539a 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -34,22 +34,22 @@ you're able to just do this: .. code-block:: python - # dialogs are the "conversations you have open" - # this method returns a list of Dialog, which - # have the .entity attribute and other information. + # Dialogs are the "conversations you have open". + # This method returns a list of Dialog, which + # has the .entity attribute and other information. dialogs = client.get_dialogs(limit=200) - # all of these work and do the same + # All of these work and do the same. lonami = client.get_entity('lonami') lonami = client.get_entity('t.me/lonami') lonami = client.get_entity('https://telegram.dog/lonami') - # other kind of entities + # Other kind of entities. channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') contact = client.get_entity('+34xxxxxxxxx') friend = client.get_entity(friend_id) - # using peers/input peers (note that the API may return these) + # Using Peer/InputPeer (note that the API may return these) # users, chats and channels may all have the same ID, so it's # necessary to wrap (at least) chat and channels inside Peer. from telethon.tl.types import PeerUser, PeerChat, PeerChannel @@ -79,7 +79,7 @@ possible, making zero API calls most of the time. When a request is made, if you provided the full entity, e.g. an ``User``, the library will convert it to the required ``InputPeer`` automatically for you. -**You should always favour ``.get_input_entity()``** over ``.get_entity()`` +**You should always favour** ``.get_input_entity()`` **over** ``.get_entity()`` for this reason! Calling the latter will always make an API call to get the most recent information about said entity, but invoking requests don't need this information, just the ``InputPeer``. Only use ``.get_entity()`` diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index de0b3baf..88a6247c 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -50,8 +50,7 @@ Basic Usage client.send_file('username', '/home/myself/Pictures/holidays.jpg') client.download_profile_photo(me) - total, messages, senders = client.get_message_history('username') + messages = client.get_message_history('username') client.download_media(messages[0]) **More details**: :ref:`telegram-client` - diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index 03aed393..b4fb1ac2 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -65,4 +65,3 @@ __ https://github.com/ricmoo/pyaes __ https://pypi.python.org/pypi/pyaes __ https://github.com/sybrenstuvel/python-rsa/ __ https://pypi.python.org/pypi/rsa/3.4.2 -__ https://github.com/LonamiWebs/Telethon/issues/199 diff --git a/readthedocs/extra/developing/api-status.rst b/readthedocs/extra/developing/api-status.rst index b5092dad..492340a4 100644 --- a/readthedocs/extra/developing/api-status.rst +++ b/readthedocs/extra/developing/api-status.rst @@ -34,7 +34,7 @@ instance through ``curl``. A JSON response will be returned. curl https://rpc.pwrtelegram.xyz/?for=messages.sendMessage -**Number of ``RPC_CALL_FAIL``\ 's**: +**Number of** ``RPC_CALL_FAIL``: .. code:: bash diff --git a/readthedocs/extra/troubleshooting/rpc-errors.rst b/readthedocs/extra/troubleshooting/rpc-errors.rst index 3618fb9a..55a21d7b 100644 --- a/readthedocs/extra/troubleshooting/rpc-errors.rst +++ b/readthedocs/extra/troubleshooting/rpc-errors.rst @@ -7,8 +7,8 @@ RPC stands for Remote Procedure Call, and when Telethon raises an methods incorrectly (wrong parameters, wrong permissions, or even something went wrong on Telegram's server). The most common are: -- ``FloodError`` (420), the same request was repeated many times. Must - wait ``.seconds``. +- ``FloodWaitError`` (420), the same request was repeated many times. + Must wait ``.seconds`` (you can access this parameter). - ``SessionPasswordNeededError``, if you have setup two-steps verification on Telegram. - ``CdnFileTamperedError``, if the media you were trying to download diff --git a/readthedocs/extra/wall-of-shame.rst b/readthedocs/extra/wall-of-shame.rst index b3c9a028..95ad3e04 100644 --- a/readthedocs/extra/wall-of-shame.rst +++ b/readthedocs/extra/wall-of-shame.rst @@ -1,3 +1,8 @@ +============= +Wall of Shame +============= + + This project has an `issues `__ section for you to file **issues** whenever you encounter any when working with the diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3be2ac62..7d17cad1 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -59,11 +59,66 @@ from .extensions import markdown class TelegramClient(TelegramBareClient): - """Full featured TelegramClient meant to extend the basic functionality - + """ + Initializes the Telegram client with the specified API ID and Hash. - As opposed to the TelegramBareClient, this one features downloading - media from different data centers, starting a second thread to - handle updates, and some very common functionality. + Args: + session (:obj:`str` | :obj:`Session` | :obj:`None`): + The file name of the session file to be used if a string is + given (it may be a full path), or the Session instance to be + used otherwise. If it's ``None``, the session will not be saved, + and you should call :meth:`.log_out()` when you're done. + + api_id (:obj:`int` | :obj:`str`): + The API ID you obtained from https://my.telegram.org. + + api_hash (:obj:`str`): + The API ID you obtained from https://my.telegram.org. + + connection_mode (:obj:`ConnectionMode`, optional): + The connection mode to be used when creating a new connection + to the servers. Defaults to the ``TCP_FULL`` mode. + This will only affect how messages are sent over the network + and how much processing is required before sending them. + + use_ipv6 (:obj:`bool`, optional): + Whether to connect to the servers through IPv6 or not. + By default this is ``False`` as IPv6 support is not + too widespread yet. + + proxy (:obj:`tuple` | :obj:`dict`, optional): + A tuple consisting of ``(socks.SOCKS5, 'host', port)``. + See https://github.com/Anorov/PySocks#usage-1 for more. + + update_workers (:obj:`int`, optional): + If specified, represents how many extra threads should + be spawned to handle incoming updates, and updates will + be kept in memory until they are processed. Note that + you must set this to at least ``0`` if you want to be + able to process updates through :meth:`updates.poll()`. + + timeout (:obj:`int` | :obj:`float` | :obj:`timedelta`, optional): + The timeout to be used when receiving responses from + the network. Defaults to 5 seconds. + + spawn_read_thread (:obj:`bool`, optional): + Whether to use an extra background thread or not. Defaults + to ``True`` so receiving items from the network happens + instantly, as soon as they arrive. Can still be disabled + if you want to run the library without any additional thread. + + Kwargs: + Extra parameters will be forwarded to the ``Session`` file. + Most relevant parameters are: + + .. code-block:: python + + device_model = platform.node() + system_version = platform.system() + app_version = TelegramClient.__version__ + lang_code = 'en' + system_lang_code = lang_code + report_errors = True """ # region Initialization @@ -76,42 +131,6 @@ class TelegramClient(TelegramBareClient): timeout=timedelta(seconds=5), spawn_read_thread=True, **kwargs): - """Initializes the Telegram client with the specified API ID and Hash. - - Session can either be a `str` object (filename for the .session) - or it can be a `Session` instance (in which case list_sessions() - would probably not work). Pass 'None' for it to be a temporary - session - remember to '.log_out()'! - - The 'connection_mode' should be any value under ConnectionMode. - This will only affect how messages are sent over the network - and how much processing is required before sending them. - - The integer 'update_workers' represents depending on its value: - is None: Updates will *not* be stored in memory. - = 0: Another thread is responsible for calling self.updates.poll() - > 0: 'update_workers' background threads will be spawned, any - any of them will invoke all the self.updates.handlers. - - If 'spawn_read_thread', a background thread will be started once - an authorized user has been logged in to Telegram to read items - (such as updates and responses) from the network as soon as they - occur, which will speed things up. - - If you don't want to spawn any additional threads, pending updates - will be read and processed accordingly after invoking a request - and not immediately. This is useful if you don't care about updates - at all and have set 'update_workers=None'. - - If more named arguments are provided as **kwargs, they will be - used to update the Session instance. Most common settings are: - device_model = platform.node() - system_version = platform.system() - app_version = TelegramClient.__version__ - lang_code = 'en' - system_lang_code = lang_code - report_errors = True - """ super().__init__( session, api_id, api_hash, connection_mode=connection_mode, @@ -134,13 +153,17 @@ class TelegramClient(TelegramBareClient): # region Authorization requests def send_code_request(self, phone, force_sms=False): - """Sends a code request to the specified phone number. + """ + Sends a code request to the specified phone number. - :param str | int phone: - The phone to which the code will be sent. - :param bool force_sms: - Whether to force sending as SMS. - :return auth.SentCode: + Args: + phone (:obj:`str` | :obj:`int`): + The phone to which the code will be sent. + + force_sms (:obj:`bool`, optional): + Whether to force sending as SMS. + + Returns: Information about the result of the request. """ phone = utils.parse_phone(phone) or self._phone @@ -165,23 +188,30 @@ class TelegramClient(TelegramBareClient): Starts or completes the sign in process with the given phone number or code that Telegram sent. - :param str | int phone: - The phone to send the code to if no code was provided, or to - override the phone that was previously used with these requests. - :param str | int code: - The code that Telegram sent. - :param str password: - 2FA password, should be used if a previous call raised - SessionPasswordNeededError. - :param str bot_token: - Used to sign in as a bot. Not all requests will be available. - This should be the hash the @BotFather gave you. - :param str phone_code_hash: - The hash returned by .send_code_request. This can be set to None - to use the last hash known. + Args: + phone (:obj:`str` | :obj:`int`): + The phone to send the code to if no code was provided, + or to override the phone that was previously used with + these requests. - :return auth.SentCode | User: - The signed in user, or the information about .send_code_request(). + code (:obj:`str` | :obj:`int`): + The code that Telegram sent. + + password (:obj:`str`): + 2FA password, should be used if a previous call raised + SessionPasswordNeededError. + + bot_token (:obj:`str`): + Used to sign in as a bot. Not all requests will be available. + This should be the hash the @BotFather gave you. + + phone_code_hash (:obj:`str`): + The hash returned by .send_code_request. This can be set to None + to use the last hash known. + + Returns: + The signed in user, or the information about + :meth:`.send_code_request()`. """ if phone and not code: @@ -229,10 +259,18 @@ class TelegramClient(TelegramBareClient): Signs up to Telegram if you don't have an account yet. You must call .send_code_request(phone) first. - :param str | int code: The code sent by Telegram - :param str first_name: The first name to be used by the new account. - :param str last_name: Optional last name. - :return User: The new created user. + Args: + code (:obj:`str` | :obj:`int`): + The code sent by Telegram + + first_name (:obj:`str`): + The first name to be used by the new account. + + last_name (:obj:`str`, optional) + Optional last name. + + Returns: + The new created user. """ result = self(SignUpRequest( phone_number=self._phone, @@ -246,9 +284,11 @@ class TelegramClient(TelegramBareClient): return result.user def log_out(self): - """Logs out Telegram and deletes the current *.session file. + """ + Logs out Telegram and deletes the current *.session file. - :return bool: True if the operation was successful. + Returns: + True if the operation was successful. """ try: self(LogOutRequest()) @@ -265,7 +305,8 @@ class TelegramClient(TelegramBareClient): Gets "me" (the self user) which is currently authenticated, or None if the request fails (hence, not authenticated). - :return User: Your own user. + Returns: + Your own user. """ try: return self(GetUsersRequest([InputUserSelf()]))[0] @@ -284,19 +325,24 @@ class TelegramClient(TelegramBareClient): """ Gets N "dialogs" (open "chats" or conversations with other people). - :param limit: - How many dialogs to be retrieved as maximum. Can be set to None - to retrieve all dialogs. Note that this may take whole minutes - if you have hundreds of dialogs, as Telegram will tell the library - to slow down through a FloodWaitError. - :param offset_date: - The offset date to be used. - :param offset_id: - The message ID to be used as an offset. - :param offset_peer: - The peer to be used as an offset. + Args: + limit (:obj:`int` | :obj:`None`): + How many dialogs to be retrieved as maximum. Can be set to + ``None`` to retrieve all dialogs. Note that this may take + whole minutes if you have hundreds of dialogs, as Telegram + will tell the library to slow down through a + ``FloodWaitError``. - :return UserList[telethon.tl.custom.Dialog]: + offset_date (:obj:`datetime`, optional): + The offset date to be used. + + offset_id (:obj:`int`, optional): + The message ID to be used as an offset. + + offset_peer (:obj:`InputPeer`, optional): + The peer to be used as an offset. + + Returns: A list dialogs, with an additional .total attribute on the list. """ limit = float('inf') if limit is None else int(limit) @@ -351,11 +397,10 @@ class TelegramClient(TelegramBareClient): """ Gets all open draft messages. - 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 + Returns: + A list of custom ``Draft`` objects that are easy to work with: + You can call :meth:`draft.set_message('text')` to change the message, + or delete it through :meth:`draft.delete()`. """ response = self(GetAllDraftsRequest()) self.session.process_entities(response) @@ -365,6 +410,7 @@ class TelegramClient(TelegramBareClient): @staticmethod def _get_response_message(request, result): + """Extracts the response message known a request and Update result""" # Telegram seems to send updateMessageID first, then updateNewMessage, # however let's not rely on that just in case. msg_id = None @@ -388,19 +434,26 @@ class TelegramClient(TelegramBareClient): """ Sends the given message to the specified entity (user/chat/channel). - :param str | int | User | Chat | Channel entity: - To who will it be sent. - :param str message: - The message to be sent. - :param int | Message reply_to: - Whether to reply to a message or not. - :param str parse_mode: - Can be 'md' or 'markdown' for markdown-like parsing, in a similar - fashion how official clients work. - :param link_preview: - Should the link preview be shown? + Args: + entity (:obj:`entity`): + To who will it be sent. - :return Message: the sent message + message (:obj:`str`): + The message to be sent. + + reply_to (:obj:`int` | :obj:`Message`, optional): + Whether to reply to a message or not. If an integer is provided, + it should be the ID of the message that it should reply to. + + parse_mode (:obj:`str`, optional): + Can be 'md' or 'markdown' for markdown-like parsing, in a similar + fashion how official clients work. + + link_preview (:obj:`bool`, optional): + Should the link preview be shown? + + Returns: + the sent message """ entity = self.get_input_entity(entity) if parse_mode: @@ -435,21 +488,25 @@ class TelegramClient(TelegramBareClient): def delete_messages(self, entity, message_ids, revoke=True): """ - Deletes a message from a chat, optionally "for everyone" with argument - `revoke` set to `True`. + Deletes a message from a chat, optionally "for everyone". - The `revoke` argument has no effect for Channels and Megagroups, - where it inherently behaves as being `True`. + Args: + entity (:obj:`entity`): + From who the message will be deleted. This can actually + be ``None`` for normal chats, but **must** be present + for channels and megagroups. - Note: The `entity` argument can be `None` for normal chats, but it's - mandatory to delete messages from Channels and Megagroups. It is also - possible to supply a chat_id which will be automatically resolved to - the right type of InputPeer. + message_ids (:obj:`list` | :obj:`int` | :obj:`Message`): + The IDs (or ID) or messages to be deleted. - :param entity: ID or Entity of the chat - :param list message_ids: ID(s) or `Message` object(s) of the message(s) to delete - :param revoke: Delete the message for everyone or just this client - :returns .messages.AffectedMessages: Messages affected by deletion. + revoke (:obj:`bool`, optional): + Whether the message should be deleted for everyone or not. + By default it has the opposite behaviour of official clients, + and it will delete the message for everyone. + This has no effect on channels or megagroups. + + Returns: + The affected messages. """ if not isinstance(message_ids, list): @@ -477,34 +534,45 @@ class TelegramClient(TelegramBareClient): """ Gets the message history for the specified entity - :param entity: - The entity from whom to retrieve the message history. - :param limit: - Number of messages to be retrieved. Due to limitations with the API - retrieving more than 3000 messages will take longer than half a - minute (or even more based on previous calls). The limit may also - be None, which would eventually return the whole history. - :param offset_date: - Offset date (messages *previous* to this date will be retrieved). - :param offset_id: - Offset message ID (only messages *previous* to the given ID will - be retrieved). - :param max_id: - All the messages with a higher (newer) ID or equal to this will - be excluded - :param min_id: - All the messages with a lower (older) ID or equal to this will - be excluded. - :param add_offset: - Additional message offset - (all of the specified offsets + this offset = older messages). + Args: + entity (:obj:`entity`): + The entity from whom to retrieve the message history. - :return: A list of messages with extra attributes: - .total = (on the list) total amount of messages sent - .sender = entity of the sender - .fwd_from.sender = if fwd_from, who sent it originally - .fwd_from.channel = if fwd_from, original channel - .to = entity to which the message was sent + limit (:obj:`int` | :obj:`None`, optional): + Number of messages to be retrieved. Due to limitations with + the API retrieving more than 3000 messages will take longer + than half a minute (or even more based on previous calls). + The limit may also be ``None``, which would eventually return + the whole history. + + offset_date (:obj:`datetime`): + Offset date (messages *previous* to this date will be + retrieved). Exclusive. + + offset_id (:obj:`int`): + Offset message ID (only messages *previous* to the given + ID will be retrieved). Exclusive. + + max_id (:obj:`int`): + All the messages with a higher (newer) ID or equal to this will + be excluded + + min_id (:obj:`int`): + All the messages with a lower (older) ID or equal to this will + be excluded. + + add_offset (:obj:`int`): + Additional message offset (all of the specified offsets + + this offset = older messages). + + Returns: + A list of messages with extra attributes: + + * ``.total`` = (on the list) total amount of messages sent. + * ``.sender`` = entity of the sender. + * ``.fwd_from.sender`` = if fwd_from, who sent it originally. + * ``.fwd_from.channel`` = if fwd_from, original channel. + * ``.to`` = entity to which the message was sent. """ entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) @@ -585,11 +653,16 @@ class TelegramClient(TelegramBareClient): Sends a "read acknowledge" (i.e., notifying the given peer that we've read their messages, also known as the "double check"). - :param entity: The chat where these messages are located. - :param message: Either a list of messages or a single message. - :param max_id: Overrides messages, until which message should the - acknowledge should be sent. - :return: + Args: + entity (:obj:`entity`): + The chat where these messages are located. + + message (:obj:`list` | :obj:`Message`): + Either a list of messages or a single message. + + max_id (:obj:`int`): + Overrides messages, until which message should the + acknowledge should be sent. """ if max_id is None: if not messages: @@ -636,35 +709,47 @@ class TelegramClient(TelegramBareClient): """ Sends a file to the specified entity. - :param entity: - Who will receive the file. - :param file: - The path of the file, byte array, or stream that will be sent. - 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". + Args: + entity (:obj:`entity`): + Who will receive the file. - Subsequent calls with the very same file will result in - immediate uploads, unless .clear_file_cache() is called. - :param caption: - Optional caption for the sent media message. - :param force_document: - If left to False and the file is a path that ends with .png, .jpg - and such, the file will be sent as a photo. Otherwise always as - a document. - :param progress_callback: - A callback function accepting two parameters: (sent bytes, total) - :param reply_to: - Same as reply_to from .send_message(). - :param attributes: - Optional attributes that override the inferred ones, like - DocumentAttributeFilename and so on. - :param thumb: - Optional thumbnail (for videos). - :param kwargs: + file (:obj:`str` | :obj:`bytes` | :obj:`file`): + The path of the file, byte array, or stream that will be sent. + 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". + + Subsequent calls with the very same file will result in + immediate uploads, unless ``.clear_file_cache()`` is called. + + caption (:obj:`str`, optional): + Optional caption for the sent media message. + + force_document (:obj:`bool`, optional): + If left to ``False`` and the file is a path that ends with + ``.png``, ``.jpg`` and such, the file will be sent as a photo. + Otherwise always as a document. + + progress_callback (:obj:`callable`, optional): + A callback function accepting two parameters: + ``(sent bytes, total)``. + + reply_to (:obj:`int` | :obj:`Message`): + Same as reply_to from .send_message(). + + attributes (:obj:`list`, optional): + Optional attributes that override the inferred ones, like + ``DocumentAttributeFilename`` and so on. + + thumb (:obj:`str` | :obj:`bytes` | :obj:`file`): + Optional thumbnail (for videos). + + Kwargs: 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. - :return: + + Returns: + The message containing the sent file. """ as_photo = False if isinstance(file, str): @@ -766,15 +851,19 @@ class TelegramClient(TelegramBareClient): """ Downloads the profile photo of the given entity (user/chat/channel). - :param entity: - From who the photo will be downloaded. - :param file: - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - :param download_big: - Whether to use the big version of the available photos. - :return: - None if no photo was provided, or if it was Empty. On success + Args: + entity (:obj:`entity`): + From who the photo will be downloaded. + + file (:obj:`str` | :obj:`file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + + download_big (:obj:`bool`, optional): + Whether to use the big version of the available photos. + + Returns: + ``None`` if no photo was provided, or if it was Empty. On success the file path is returned since it may differ from the one given. """ photo = entity @@ -843,14 +932,21 @@ class TelegramClient(TelegramBareClient): def download_media(self, message, file=None, progress_callback=None): """ Downloads the given media, or the media from a specified Message. - :param message: + + message (:obj:`Message` | :obj:`Media`): The media or message containing the media that will be downloaded. - :param file: + + file (:obj:`str` | :obj:`file`, optional): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. - :param progress_callback: - A callback function accepting two parameters: (recv bytes, total) - :return: + + progress_callback (:obj:`callable`, optional): + A callback function accepting two parameters: + ``(recv bytes, total)``. + + Returns: + ``None`` if no media was provided, or if it was Empty. On success + the file path is returned since it may differ from the one given. """ # TODO This won't work for messageService if isinstance(message, Message): @@ -1038,7 +1134,7 @@ class TelegramClient(TelegramBareClient): """ Turns the given entity into a valid Telegram user or chat. - :param entity: + entity (:obj:`str` | :obj:`int` | :obj:`Peer` | :obj:`InputPeer`): The entity (or iterable of entities) to be transformed. If it's a string which can be converted to an integer or starts with '+' it will be resolved as if it were a phone number. @@ -1053,7 +1149,9 @@ class TelegramClient(TelegramBareClient): If the entity is neither, and it's not a TLObject, an error will be raised. - :return: User, Chat or Channel corresponding to the input entity. + Returns: + ``User``, ``Chat`` or ``Channel`` corresponding to the input + entity. """ if not isinstance(entity, str) and hasattr(entity, '__iter__'): single = False @@ -1145,19 +1243,20 @@ class TelegramClient(TelegramBareClient): use this kind of InputUser, InputChat and so on, so this is the most suitable call to make for those cases. - :param peer: + entity (:obj:`str` | :obj:`int` | :obj:`Peer` | :obj:`InputPeer`): The integer ID of an user or otherwise either of a - PeerUser, PeerChat or PeerChannel, for which to get its - Input* version. + ``PeerUser``, ``PeerChat`` or ``PeerChannel``, for + which to get its ``Input*`` version. - If this Peer hasn't been seen before by the library, the top + If this ``Peer`` hasn't been seen before by the library, the top dialogs will be loaded and their entities saved to the session file (unless this feature was disabled explicitly). If in the end the access hash required for the peer was not found, a ValueError will be raised. - :return: InputPeerUser, InputPeerChat or InputPeerChannel. + Returns: + ``InputPeerUser``, ``InputPeerChat`` or ``InputPeerChannel``. """ try: # First try to get the entity from cache, otherwise figure it out From c4e26c95f58673fb5316b649801ac16356719090 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 15:33:25 +0100 Subject: [PATCH 055/361] Always cache files smaller than 10MB, now in the database This removes the need for a .clear_cache() method as now files are identified by their MD5 (which needs to be calculated always) and their file size (to make collisions even more unlikely) instead using the file path (which can now change). --- telethon/telegram_bare_client.py | 56 +++++++++++++++++++------------- telethon/telegram_client.py | 17 ++-------- telethon/tl/session.py | 44 +++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 41 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 93453322..ab6d3bbb 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -158,9 +158,6 @@ class TelegramBareClient: # See https://core.telegram.org/api/invoking#saving-client-info. self._first_request = True - # Uploaded files cache so subsequent calls are instant - self._upload_cache = {} - # Constantly read for results and updates from within the main client, # if the user has left enabled such option. self._spawn_read_thread = spawn_read_thread @@ -639,6 +636,7 @@ class TelegramBareClient: file = file.read() file_size = len(file) + # File will now either be a string or bytes if not part_size_kb: part_size_kb = get_appropriated_part_size(file_size) @@ -649,18 +647,40 @@ class TelegramBareClient: if part_size % 1024 != 0: raise ValueError('The part size must be evenly divisible by 1024') + # Set a default file name if None was specified + file_id = utils.generate_random_long() + if not file_name: + if isinstance(file, str): + file_name = os.path.basename(file) + else: + file_name = str(file_id) + # Determine whether the file is too big (over 10MB) or not # Telegram does make a distinction between smaller or larger files is_large = file_size > 10 * 1024 * 1024 + if not is_large: + # Calculate the MD5 hash before anything else. + # As this needs to be done always for small files, + # might as well do it before anything else and + # check the cache. + if isinstance(file, str): + with open(file, 'rb') as stream: + file = stream.read() + hash_md5 = md5(file) + tuple_ = self.session.get_file(hash_md5.digest(), file_size) + if tuple_: + __log__.info('File was already cached, not uploading again') + return InputFile(name=file_name, + md5_checksum=tuple_[0], id=tuple_[2], parts=tuple_[3]) + else: + hash_md5 = None + part_count = (file_size + part_size - 1) // part_size - - file_id = utils.generate_random_long() - hash_md5 = md5() - __log__.info('Uploading file of %d bytes in %d chunks of %d', file_size, part_count, part_size) - stream = open(file, 'rb') if isinstance(file, str) else BytesIO(file) - try: + + with open(file, 'rb') if isinstance(file, str) else BytesIO(file) \ + as stream: for part_index in range(part_count): # Read the file by in chunks of size part_size part = stream.read(part_size) @@ -675,29 +695,19 @@ class TelegramBareClient: result = self(request) if result: - __log__.debug('Uploaded %d/%d', part_index, part_count) - if not is_large: - # No need to update the hash if it's a large file - hash_md5.update(part) - + __log__.debug('Uploaded %d/%d', part_index + 1, part_count) if progress_callback: progress_callback(stream.tell(), file_size) else: raise RuntimeError( 'Failed to upload file part {}.'.format(part_index)) - finally: - stream.close() - - # Set a default file name if None was specified - if not file_name: - if isinstance(file, str): - file_name = os.path.basename(file) - else: - file_name = str(file_id) if is_large: return InputFileBig(file_id, part_count, file_name) else: + self.session.cache_file( + hash_md5.digest(), file_size, file_id, part_count) + return InputFile(file_id, part_count, file_name, md5_checksum=hash_md5.hexdigest()) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 7d17cad1..7b8a84fa 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -759,13 +759,8 @@ class TelegramClient(TelegramBareClient): for ext in ('.png', '.jpg', '.gif', '.jpeg') ) - file_hash = hash(file) - if file_hash in self._upload_cache: - file_handle = self._upload_cache[file_hash] - else: - self._upload_cache[file_hash] = file_handle = self.upload_file( - file, progress_callback=progress_callback - ) + file_handle = self.upload_file( + file, progress_callback=progress_callback) if as_photo and not force_document: media = InputMediaUploadedPhoto(file_handle, caption) @@ -835,14 +830,6 @@ class TelegramClient(TelegramBareClient): reply_to=reply_to, is_voice_note=()) # empty tuple is enough - def clear_file_cache(self): - """Calls to .send_file() will cache the remote location of the - uploaded files so that subsequent files can be immediate, so - uploading the same file path will result in using the cached - version. To avoid this a call to this method should be made. - """ - self._upload_cache.clear() - # endregion # region Downloading media requests diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 3fa13d23..59794f16 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -2,7 +2,6 @@ import json import os import platform import sqlite3 -import struct import time from base64 import b64decode from os.path import isfile as file_exists @@ -16,7 +15,7 @@ from ..tl.types import ( ) EXTENSION = '.session' -CURRENT_VERSION = 1 # database version +CURRENT_VERSION = 2 # database version class Session: @@ -93,6 +92,8 @@ class Session: version = c.fetchone()[0] if version != CURRENT_VERSION: self._upgrade_database(old=version) + c.execute("delete from version") + c.execute("insert into version values (?)", (CURRENT_VERSION,)) self.save() # These values will be saved @@ -125,6 +126,17 @@ class Session: name text ) without rowid""" ) + # Save file_size along with md5_digest + # to make collisions even more unlikely. + c.execute( + """create table sent_files ( + md5_digest blob, + file_size integer, + file_id integer, + part_count integer, + primary key(md5_digest, file_size) + ) without rowid""" + ) # Migrating from JSON -> new table and may have entities if entities: c.executemany( @@ -158,7 +170,17 @@ class Session: return [] # No entities def _upgrade_database(self, old): - pass + if old == 1: + self._conn.execute( + """create table sent_files ( + md5_digest blob, + file_size integer, + file_id integer, + part_count integer, + primary key(md5_digest, file_size) + ) without rowid""" + ) + old = 2 # Data from sessions should be kept as properties # not to fetch the database every time we need it @@ -370,3 +392,19 @@ class Session: return InputPeerChannel(i, h) else: raise ValueError('Could not find input entity with key ', key) + + # File processing + + def get_file(self, md5_digest, file_size): + return self._conn.execute( + 'select * from sent_files ' + 'where md5_digest = ? and file_size = ?', (md5_digest, file_size) + ).fetchone() + + def cache_file(self, md5_digest, file_size, file_id, part_count): + with self._db_lock: + self._conn.execute( + 'insert into sent_files values (?,?,?,?)', + (md5_digest, file_size, file_id, part_count) + ) + self.save() From ec4ca5dbfc6e6e7e3d7275eb8c19745daf1d7b63 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 18:31:48 +0100 Subject: [PATCH 056/361] More consistent with asyncio branch (style/small fixes) Like passing an extra (invalid) dt parameter when serializing a datetime, and handling more errors in the TcpClient class. --- telethon/extensions/markdown.py | 4 ++-- telethon/extensions/tcp_client.py | 20 +++++++++++++++----- telethon/telegram_client.py | 21 ++++----------------- telethon/tl/tlobject.py | 2 +- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 24ae5aa7..6285bf28 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -192,10 +192,10 @@ def get_inner_text(text, entity): :param entity: the entity or entities that must be matched. :return: a single result or a list of the text surrounded by the entities. """ - if not isinstance(entity, TLObject) and hasattr(entity, '__iter__'): + if isinstance(entity, TLObject): + entity = (entity,) multiple = True else: - entity = [entity] multiple = False text = text.encode(ENC) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 61be30f5..e67c032c 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -3,10 +3,17 @@ This module holds a rough implementation of the C# TCP client. """ import errno import socket +import time from datetime import timedelta from io import BytesIO, BufferedWriter from threading import Lock +MAX_TIMEOUT = 15 # in seconds +CONN_RESET_ERRNOS = { + errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH, + errno.EINVAL, errno.ENOTCONN +} + class TcpClient: """A simple TCP client to ease the work with sockets and proxies.""" @@ -59,6 +66,7 @@ class TcpClient: else: mode, address = socket.AF_INET, (ip, port) + timeout = 1 while True: try: while not self._socket: @@ -69,10 +77,12 @@ class TcpClient: except OSError as e: # There are some errors that we know how to handle, and # the loop will allow us to retry - if e.errno == errno.EBADF: + if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL): # Bad file descriptor, i.e. socket was closed, set it # to none to recreate it on the next iteration self._socket = None + time.sleep(timeout) + timeout = min(timeout * 2, MAX_TIMEOUT) else: raise @@ -105,7 +115,7 @@ class TcpClient: :param data: the data to send. """ if self._socket is None: - raise ConnectionResetError() + self._raise_connection_reset() # TODO Timeout may be an issue when sending the data, Changed in v3.5: # The socket timeout is now the maximum total duration to send all data. @@ -116,7 +126,7 @@ class TcpClient: except ConnectionError: self._raise_connection_reset() except OSError as e: - if e.errno == errno.EBADF: + if e.errno in CONN_RESET_ERRNOS: self._raise_connection_reset() else: raise @@ -129,7 +139,7 @@ class TcpClient: :return: the read data with len(data) == size. """ if self._socket is None: - raise ConnectionResetError() + self._raise_connection_reset() # TODO Remove the timeout from this method, always use previous one with BufferedWriter(BytesIO(), buffer_size=size) as buffer: @@ -142,7 +152,7 @@ class TcpClient: except ConnectionError: self._raise_connection_reset() except OSError as e: - if e.errno == errno.EBADF or e.errno == errno.ENOTSOCK: + if e.errno in CONN_RESET_ERRNOS: self._raise_connection_reset() else: raise diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 7b8a84fa..2f9eaecf 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -317,10 +317,7 @@ class TelegramClient(TelegramBareClient): # region Dialogs ("chats") requests - def get_dialogs(self, - limit=10, - offset_date=None, - offset_id=0, + def get_dialogs(self, limit=10, offset_date=None, offset_id=0, offset_peer=InputPeerEmpty()): """ Gets N "dialogs" (open "chats" or conversations with other people). @@ -425,11 +422,7 @@ class TelegramClient(TelegramBareClient): if update.message.id == msg_id: return update.message - def send_message(self, - entity, - message, - reply_to=None, - parse_mode=None, + def send_message(self, entity, message, reply_to=None, parse_mode=None, link_preview=True): """ Sends the given message to the specified entity (user/chat/channel). @@ -523,14 +516,8 @@ class TelegramClient(TelegramBareClient): else: return self(messages.DeleteMessagesRequest(message_ids, revoke=revoke)) - def get_message_history(self, - entity, - limit=20, - offset_date=None, - offset_id=0, - max_id=0, - min_id=0, - add_offset=0): + def get_message_history(self, entity, limit=20, offset_date=None, + offset_id=0, max_id=0, min_id=0, add_offset=0): """ Gets the message history for the specified entity diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 0ed7b015..ad930f9c 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -134,7 +134,7 @@ class TLObject: if isinstance(dt, datetime): dt = int(dt.timestamp()) elif isinstance(dt, date): - dt = int(datetime(dt.year, dt.month, dt.day, dt).timestamp()) + dt = int(datetime(dt.year, dt.month, dt.day).timestamp()) elif isinstance(dt, float): dt = int(dt) From 4871a6fb96dc740900cdfee67617758fb9b2f633 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 19:51:44 +0100 Subject: [PATCH 057/361] Accept 'me' and 'self' usernames to get self user entity --- telethon/telegram_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 2f9eaecf..6ec8fd02 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -52,7 +52,7 @@ from .tl.types import ( InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, - ChatInvite, ChatInviteAlready, PeerChannel, Photo + ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf ) from .tl.types.messages import DialogsSlice from .extensions import markdown @@ -1202,6 +1202,8 @@ class TelegramClient(TelegramBareClient): elif isinstance(invite, ChatInviteAlready): return invite.chat else: + if string in ('me', 'self'): + return self.get_me() result = self(ResolveUsernameRequest(string)) for entity in itertools.chain(result.users, result.chats): if entity.username.lower() == string: @@ -1239,6 +1241,8 @@ class TelegramClient(TelegramBareClient): pass if isinstance(peer, str): + if peer in ('me', 'self'): + return InputPeerSelf() return utils.get_input_peer(self._get_entity_from_string(peer)) is_peer = False From 60594920bd0e1d56822f2474bfb56894c62a40e1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 23:19:58 +0100 Subject: [PATCH 058/361] Add changelog from GitHub releases to RTD --- readthedocs/extra/changelog.rst | 1285 +++++++++++++++++++++++++++++++ readthedocs/index.rst | 3 +- 2 files changed, 1287 insertions(+), 1 deletion(-) create mode 100644 readthedocs/extra/changelog.rst diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst new file mode 100644 index 00000000..96ab594c --- /dev/null +++ b/readthedocs/extra/changelog.rst @@ -0,0 +1,1285 @@ +.. _changelog: + + +=========================== +Changelog (Version History) +=========================== + + +This page lists all the available versions of the library, +in chronological order. You should read this when upgrading +the library to know where your code can break, and where +it can take advantage of new goodies! + +.. contents:: List of All Versions + + +Sessions as sqlite databases (v0.16) +==================================== + +*Published at 2017/12/28* + +In the beginning, session files used to be pickle. This proved to be bad +as soon as one wanted to add more fields. For this reason, they were +migrated to use JSON instead. But this proved to be bad as soon as one +wanted to save things like entities (usernames, their ID and hash), so +now it properly uses +`sqlite3 `__, +which has been well tested, to save the session files! Calling +``.get_input_entity`` using a ``username`` no longer will need to fetch +it first, so it's really 0 calls again. Calling ``.get_entity`` will +always fetch the most up to date version. + +Furthermore, nearly everything has been documented, thus preparing the +library for `Read the Docs `__ (although there +are a few things missing I'd like to polish first), and the +`logging `__ are now +better placed. + +Breaking changes +~~~~~~~~~~~~~~~~ + +- ``.get_dialogs()`` now returns a **single list** instead a tuple + consisting of a **custom class** that should make everything easier + to work with. +- ``.get_message_history()`` also returns a **single list** instead a + tuple, with the ``Message`` instances modified to make them more + convenient. + +Both lists have a ``.total`` attribute so you can still know how many +dialogs/messages are in total. + +New stuff +~~~~~~~~~ + +- The mentioned use of ``sqlite3`` for the session file. +- ``.get_entity()`` now supports lists too, and it will make as little + API calls as possible if you feed it ``InputPeer`` types. Usernames + will always be resolved, since they may have changed. +- ``.set_proxy()`` method, to avoid having to create a new + ``TelegramClient``. +- More ``date`` types supported to represent a date parameter. + +Bug fixes +~~~~~~~~~ + +- Empty strings weren't working when they were a flag parameter (e.g., + setting no last name). +- Fix invalid assertion regarding flag parameters as well. +- Avoid joining the background thread on disconnect, as it would be + ``None`` due to a race condition. +- Correctly handle ``None`` dates when downloading media. +- ``.download_profile_photo`` was failing for some channels. +- ``.download_media`` wasn't handling ``Photo``. + +Internal changes +~~~~~~~~~~~~~~~~ + +- ``date`` was being serialized as local date, but that was wrong. +- ``date`` was being represented as a ``float`` instead of an ``int``. +- ``.tl`` parser wasn't stripping inline comments. +- Removed some redundant checks on ``update_state.py``. +- Use a `synchronized + queue `__ instead a + hand crafted version. +- Use signed integers consistently (e.g. ``salt``). +- Always read the corresponding ``TLObject`` from API responses, except + for some special cases still. +- A few more ``except`` low level to correctly wrap errors. +- More accurate exception types. +- ``invokeWithLayer(initConnection(X))`` now wraps every first request + after ``.connect()``. + +As always, report if you have issues with some of the changes! + +IPv6 support (v0.15.5) +====================== + +*Published at 2017/11/16* + ++-----------------------+ +| Scheme layer used: 73 | ++-----------------------+ + +It's here, it has come! The library now **supports IPv6**! Just pass +``use_ipv6=True`` when creating a ``TelegramClient``. Note that I could +*not* test this feature because my machine doesn't have IPv6 setup. If +you know IPv6 works in your machine but the library doesn't, please +refer to `#425 `_. + +Additions +~~~~~~~~~ + +- IPv6 support. +- New method to extract the text surrounded by ``MessageEntity``\ 's, + in the ``extensions.markdown`` module. + +Enhancements +~~~~~~~~~~~~ + +- Markdown parsing is Done Right. +- Reconnection on failed invoke. Should avoid "number of retries + reached 0" (#270). +- Some missing autocast to ``Input*`` types. +- The library uses the ``NullHandler`` for ``logging`` as it should + have always done. +- ``TcpClient.is_connected()`` is now more reliable. + +.. bug-fixes-1: + +Bug fixes +~~~~~~~~~ + +- Getting an entity using their phone wasn't actually working. +- Full entities aren't saved unless they have an ``access_hash``, to + avoid some ``None`` errors. +- ``.get_message_history`` was failing when retrieving items that had + messages forwarded from a channel. + +General enhancements (v0.15.4) +============================== + +*Published at 2017/11/04* + ++-----------------------+ +| Scheme layer used: 72 | ++-----------------------+ + +This update brings a few general enhancements that are enough to deserve +a new release, with a new feature: beta **markdown-like parsing** for +``.send_message()``! + +.. additions-1: + +Additions +~~~~~~~~~ + +- ``.send_message()`` supports ``parse_mode='md'`` for **Markdown**! It + works in a similar fashion to the official clients (defaults to + double underscore/asterisk, like ``**this**``). Please report any + issues with emojies or enhancements for the parser! +- New ``.idle()`` method so your main thread can do useful job (listen + for updates). +- Add missing ``.to_dict()``, ``__str__`` and ``.stringify()`` for + ``TLMessage`` and ``MessageContainer``. + +.. bug-fixes-2: + +Bug fixes +~~~~~~~~~ + +- The list of known peers could end "corrupted" and have users with + ``access_hash=None``, resulting in ``struct`` error for it not being + an integer. You shouldn't encounter this issue anymore. +- The warning for "added update handler but no workers set" wasn't + actually working. +- ``.get_input_peer`` was ignoring a case for ``InputPeerSelf``. +- There used to be an exception when logging exceptions (whoops) on + update handlers. +- "Downloading contacts" would produce strange output if they had + semicolons (``;``) in their name. +- Fix some cyclic imports and installing dependencies from the ``git`` + repository. +- Code generation was using f-strings, which are only supported on + Python ≥3.6. + +Other changes +~~~~~~~~~~~~~ + +- The ``auth_key`` generation has been moved from ``.connect()`` to + ``.invoke()``. There were some issues were ``.connect()`` failed and + the ``auth_key`` was ``None`` so this will ensure to have a valid + ``auth_key`` when needed, even if ``BrokenAuthKeyError`` is raised. +- Support for higher limits on ``.get_history()`` and + ``.get_dialogs()``. +- Much faster integer factorization when generating the required + ``auth_key``. Thanks @delivrance for making me notice this, and for + the pull request. + +Bug fixes with updates (v0.15.3) +================================ + +*Published at 2017/10/20* + +Hopefully a very ungrateful bug has been removed. When you used to +invoke some request through update handlers, it could potentially enter +an infinite loop. This has been mitigated and it's now safe to invoke +things again! A lot of updates were being dropped (all those gzipped), +and this has been fixed too. + +More bug fixes include a `correct +parsing `__ +of certain TLObjects thanks to @stek29, and +`some `__ +`wrong +calls `__ +that would cause the library to crash thanks to @andr-04, and the +``ReadThread`` not re-starting if you were already authorized. + +Internally, the ``.to_bytes()`` function has been replaced with +``__bytes__`` so now you can do ``bytes(tlobject)``. + +Bug fixes and new small features (v0.15.2) +========================================== + +*Published at 2017/10/14* + +This release primarly focuses on a few bug fixes and enhancements. +Although more stuff may have broken along the way. + +.. bug-fixes-3: + +Bug fixes: +~~~~~~~~~~ + +- ``.get_input_entity`` was failing for IDs and other cases, also + making more requests than it should. +- Use ``basename`` instead ``abspath`` when sending a file. You can now + also override the attributes. +- ``EntityDatabase.__delitem__`` wasn't working. +- ``.send_message()`` was failing with channels. +- ``.get_dialogs(limit=None)`` should now return all the dialogs + correctly. +- Temporary fix for abusive duplicated updates. + +.. enhancements-1: + +Enhancements: +~~~~~~~~~~~~~ + +- You will be warned if you call ``.add_update_handler`` with no + ``update_workers``. +- New customizable threshold value on the session to determine when to + automatically sleep on flood waits. See + ``client.session.flood_sleep_threshold``. +- New ``.get_drafts()`` method with a custom ``Draft`` class by @JosXa. +- Join all threads when calling ``.disconnect()``, to assert no + dangling thread is left alive. +- Larger chunk when downloading files should result in faster + downloads. +- You can use a callable key for the ``EntityDatabase``, so it can be + any filter you need. + +.. internal-changes-1: + +Internal changes: +~~~~~~~~~~~~~~~~~ + +- MsgsAck is now sent in a container rather than its own request. +- ``.get_input_photo`` is now used in the generated code. +- ``.process_entities`` was being called from more places than only + ``__call__``. +- ``MtProtoSender`` now relies more on the generated code to read + responses. + +Custom Entity Database (v0.15.1) +================================ + +*Published at 2017/10/05* + +The main feature of this release is that Telethon now has a custom +database for all the entities you encounter, instead depending on +``@lru_cache`` on the ``.get_entity()`` method. + +The ``EntityDatabase`` will, by default, **cache** all the users, chats +and channels you find in memory for as long as the program is running. +The session will, by default, save all key-value pairs of the entity +identifiers and their hashes (since Telegram may send an ID that it +thinks you already know about, we need to save this information). + +You can **prevent** the ``EntityDatabase`` from saving users by setting +``client.session.entities.enabled = False``, and prevent the ``Session`` +from saving input entities at all by setting +``client.session.save_entities = False``. You can also clear the cache +for a certain user through +``client.session.entities.clear_cache(entity=None)``, which will clear +all if no entity is given. + +More things: + +- ``.sign_in`` accepts phones as integers. +- ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the + right amount of dialogs. +- New method to ``.delete_messages()``. +- New ``ChannelPrivateError`` class. +- Changing the IP to which you connect to is as simple as + ``client.session.server_address = 'ip'``, since now the + server address is always queried from the session. +- ``GeneralProxyError`` should be passed to the main thread + again, so that you can handle it. + +Updates Overhaul Update (v0.15) +=============================== + +*Published at 2017/10/01* + +After hundreds of lines changed on a major refactor, *it's finally +here*. It's the **Updates Overhaul Update**; let's get right into it! + +New stuff and enhancements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- You can **invoke** requests from **update handlers**. And **any other + thread**. A new temporary will be made, so that you can be sending + even several requests at the same time! +- **Several worker threads** for your updates! By default, ``None`` + will spawn. I recommend you to work with ``update_workers=4`` to get + started, these will be polling constantly for updates. +- You can also change the number of workers at any given time. +- The library can now run **in a single thread** again, if you don't + need to spawn any at all. Simply set ``spawn_read_thread=False`` when + creating the ``TelegramClient``! +- You can specify ``limit=None`` on ``.get_dialogs()`` to get **all** + of them[1]. +- **Updates are expanded**, so you don't need to check if the update + has ``.updates`` or an inner ``.update`` anymore. +- All ``InputPeer`` entities are **saved in the session** file, but you + can disable this by setting ``save_entities=False``. +- New ``.get_input_entity`` method, which makes use of the above + feature. You **should use this** when a request needs a + ``InputPeer``, rather than the whole entity (although both work). + +Less important enhancements +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Assert that either all or None dependent-flag parameters are set + before sending the request. +- Phone numbers can have dashes, spaces, or parenthesis. They'll be + removed before making the request. +- You can override the phone and its hash on ``.sign_in()``, if you're + creating a new ``TelegramClient`` on two different places. + +Compatibility breaks +~~~~~~~~~~~~~~~~~~~~ + +- ``.create_new_connection()`` is gone for good. No need to deal with + this manually since new connections are now handled on demand by the + library itself. + +Bugs fixed +~~~~~~~~~~ + +- ``.log_out()`` was consuming all retries. It should work just fine + now. +- The session would fail to load if the ``auth_key`` had been removed + manually. +- ``Updates.check_error`` was popping wrong side, although it's been + completely removed. +- ``ServerError``\ 's will be **ignored**, and the request will + immediately be retried. +- Cross-thread safety when saving the session file. +- Some things changed on a matter of when to reconnect, so please + report any bugs! + +.. internal-changes-2: + +Internal changes +~~~~~~~~~~~~~~~~ + +- ``TelegramClient`` is now only an abstraction over the + ``TelegramBareClient``, which can only do basic things, such as + invoking requests, working with files, etc. If you don't need any of + the abstractions the ``TelegramClient``, you can now use the + ``TelegramBareClient`` in a much more comfortable way. +- ``MtProtoSender`` is not thread-safe, but it doesn't need to be since + a new connection will be spawned when needed. +- New connections used to be cached and then reused. Now only their + sessions are saved, as temporary connections are spawned only when + needed. +- Added more RPC errors to the list. + +**[1]:** Broken due to a condition which should had been the opposite +(sigh), fixed 4 commits ahead on +https://github.com/LonamiWebs/Telethon/commit/62ea77cbeac7c42bfac85aa8766a1b5b35e3a76c. + +-------------- + +**That's pretty much it**, although there's more work to be done to make +the overall experience of working with updates *even better*. Stay +tuned! + +Serialization bug fixes (v0.14.2) +================================= + +*Published at 2017/09/29* + +Two bug fixes, one of them quite **important**, related to the +serialization. Every object or request that had to serialize a +``True/False`` type was always being serialized as ``false``! + +Another bug that didn't allow you to leave as ``None`` flag parameters +that needed a list has been fixed. + +Other internal changes include a somewhat more readable ``.to_bytes()`` +function and pre-computing the flag instead using bit shifting. The +``TLObject.constructor_id`` has been renamed to +``TLObject.CONSTRUCTOR_ID``, and ``.subclass_of_id`` is also uppercase +now. + +Farewell, BinaryWriter (v0.14.1) +================================ + +*Published at 2017/09/28* + +Version ``v0.14`` had started working on the new ``.to_bytes()`` method +to dump the ``BinaryWriter`` and its usage on the ``.on_send()`` when +serializing TLObjects, and this release finally removes it. The speed up +when serializing things to bytes should now be over twice as fast +wherever it's needed. + +Other internal changes include using proper classes (including the +generated code) for generating authorization keys and to write out +``TLMessage``\ 's. + +For **bug fixes**, this version is again compatible with Python 3.x +versions **below 3.5** (there was a method call that was Python 3.5 and +above). + +Several requests at once and upload compression (v0.14) +======================================================= + +*Published at 2017/09/27* + +New major release, since I've decided that these two features are big +enough: + +- Requests larger than 512 bytes will be **compressed through + gzip**, and if the result is smaller, this will be uploaded instead. +- You can now send **multiple requests at once**, they're simply + ``*var_args`` on the ``.invoke()``. Note that the server doesn't + guarantee the order in which they'll be executed! + +Internally, another important change. The ``.on_send`` function on the +``TLObjects`` is **gone**, and now there's a new ``.to_bytes()``. From +my tests, this has always been over twice as fast serializing objects, +although more replacements need to be done, so please report any issues. + +Besides this: + +- Downloading media from CDNs wasn't working (wrong + access to a parameter). +- Correct type hinting. +- Added a tiny sleep when trying to perform automatic reconnection. +- Error reporting is done in the background, and has a shorter timeout. +- Implemented ``.get_input_media`` helper methods. Now you can even use + another message as input media! +- ``setup.py`` used to fail with wrongly generated code. + +Quick fix-up (v0.13.6) +====================== + +*Published at 2017/09/23* + +Before getting any further, here's a quick fix-up with things that +should have been on ``v0.13.5`` but were missed. Specifically, the +**timeout when receiving** a request will now work properly. + +Some other additions are a tiny fix when **handling updates**, which was +ignoring some of them, nicer ``__str__`` and ``.stringify()`` methods +for the ``TLObject``\ 's, and not stopping the ``ReadThread`` if you try +invoking something there (now it simply returns ``None``). + +Attempts at more stability (v0.13.5) +==================================== + +*Published at 2017/09/23* + +Yet another update to fix some bugs and increase the stability of the +library, or, at least, that was the attempt! + +This release should really **improve the experience with the background +thread** that the library starts to read things from the network as soon +as it can, but I can't spot every use case, so please report any bug +(and as always, minimal reproducible use cases will help a lot). + +.. bug-fixes-4: + +Bug fixes +~~~~~~~~~ + +- ``setup.py`` was failing on Python < 3.5 due to some imports. +- Duplicated updates should now be ignored. +- ``.send_message`` would crash in some cases, due to having a typo + using the wrong object. +- ``"socket is None"`` when calling ``.connect()`` should not happen + anymore. +- ``BrokenPipeError`` was still being raised due to an incorrect order + on the ``try/except`` block. + +.. enhancements-2: + +Enhancements +~~~~~~~~~~~~ + +- **Type hinting** for all the generated ``Request``\ 's and + ``TLObjects``! IDEs like PyCharm will benefit from this. +- ``ProxyConnectionError`` should properly be passed to the main thread + for you to handle. +- The background thread will only be started after you're authorized on + Telegram (i.e. logged in), and several other attempts at polishing + the experience with this thread. +- The ``Connection`` instance is only created once now, and reused + later. +- Calling ``.connect()`` should have a better behavior now (like + actually *trying* to connect even if we seemingly were connected + already). +- ``.reconnect()`` behavior has been changed to also be more consistent + by making the assumption that we'll only reconnect if the server has + disconnected us, and is now private. + +.. other-changes-1: + +Other changes +~~~~~~~~~~~~~ + +- ``TLObject.__repr__`` doesn't show the original TL definition + anymore, it was a lot of clutter. If you have any complaints open an + issue and we can discuss it. +- Internally, the ``'+'`` from the phone number is now stripped, since + it shouldn't be included. +- Spotted a new place where ``BrokenAuthKeyError`` would be raised, and + it now is raised there. + +More bug fixes and enhancements (v0.13.4) +========================================= + +*Published at 2017/09/18* + +.. new-stuff-1: + +New stuff: +~~~~~~~~~~ + +- ``TelegramClient`` now exposes a ``.is_connected()`` method. +- Initial authorization on a new data center will retry up to 5 times + by default. +- Errors that couldn't be handled on the background thread will be + raised on the next call to ``.invoke()`` or ``updates.poll()``. + +.. bugs-fixed-1: + +Bugs fixed: +~~~~~~~~~~~ + +- Now you should be able to sign in even if you have + ``process_updates=True`` and no previous session. +- Some errors and methods are documented a bit clearer. +- ``.send_message()`` could randomly fail, as the returned type was not + expected. + +Things that should reduce the amount of crashes: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- ``TimeoutError`` is now ignored, since the request will be retried up + to 5 times by default. +- "-404" errors (``BrokenAuthKeyError``\ 's) are now detected when + first connecting to a new data center. +- ``BufferError`` is handled more gracefully, in the same way as + ``InvalidCheckSumError``\ 's. +- Attempt at fixing some "NoneType has no attribute…" errors (with the + ``.sender``). + +Other internal changes: +~~~~~~~~~~~~~~~~~~~~~~~ + +- Calling ``GetConfigRequest`` is now made less often. +- The ``initial_query`` parameter from ``.connect()`` is gone, as it's + not needed anymore. +- Renamed ``all_tlobjects.layer`` to ``all_tlobjects.LAYER`` (since + it's a constant). +- The message from ``BufferError`` is now more useful. + +Bug fixes and enhancements (v0.13.3) +==================================== + +*Published at 2017/09/14* + +.. bugs-fixed-2: + +Bugs fixed +---------- + +- **Reconnection** used to fail because it tried invoking things from + the ``ReadThread``. +- Inferring **random ids** for ``ForwardMessagesRequest`` wasn't + working. +- Downloading media from **CDNs** failed due to having forgotten to + remove a single line. +- ``TcpClient.close()`` now has a **``threading.Lock``**, so + ``NoneType has no close()`` should not happen. +- New **workaround** for ``msg seqno too low/high``. Also, both + ``Session.id/seq`` are not saved anymore. + +.. enhancements-3: + +Enhancements +------------ + +- **Request will be retried** up to 5 times by default rather than + failing on the first attempt. +- ``InvalidChecksumError``\ 's are now **ignored** by the library. +- ``TelegramClient.get_entity()`` is now **public**, and uses the + ``@lru_cache()`` decorator. +- New method to **``.send_voice_note()``**\ 's. +- Methods to send message and media now support a **``reply_to`` + parameter**. +- ``.send_message()`` now returns the **full message** which was just + sent. + +New way to work with updates (v0.13.2) +====================================== + +*Published at 2017/09/08* + +This update brings a new way to work with updates, and it's begging for +your **feedback**, or better names or ways to do what you can do now. + +Please refer to the `wiki/Usage +Modes `__ for +an in-depth description on how to work with updates now. Notice that you +cannot invoke requests from within handlers anymore, only the +``v.0.13.1`` patch allowed you to do so. + +**Other fixes**: + +- Periodic pings are back. +- The username regex mentioned on ``UsernameInvalidError`` was invalid, + but it has now been fixed. +- Sending a message to a phone number was failing because the type used + for a request had changed on layer 71. +- CDN downloads weren't working properly, and now a few patches have been + applied to ensure more reliability, although I couldn't personally test + this, so again, report any feedback. + +Invoke other requests from within update callbacks (v0.13.1) +============================================================ + +*Published at 2017/09/04* + +.. warning:: + + This update brings some big changes to the update system, + so please read it if you work with them! + +A silly "bug" which hadn't been spotted has now been fixed. Now you can +invoke other requests from within your update callbacks. However **this +is not advised**. You should post these updates to some other thread, +and let that thread do the job instead. Invoking a request from within a +callback will mean that, while this request is being invoked, no other +things will be read. + +Internally, the generated code now resides under a *lot* less files, +simply for the sake of avoiding so many unnecessary files. The generated +code is not meant to be read by anyone, simply to do its job. + +Unused attributes have been removed from the ``TLObject`` class too, and +``.sign_up()`` returns the user that just logged in in a similar way to +``.sign_in()`` now. + +Connection modes (v0.13) +======================== + +*Published at 2017/09/04* + ++-----------------------+ +| Scheme layer used: 71 | ++-----------------------+ + +The purpose of this release is to denote a big change, now you can +connect to Telegram through different `**connection +modes** `__. +Also, a **second thread** will *always* be started when you connect a +``TelegramClient``, despite whether you'll be handling updates or +ignoring them, whose sole purpose is to constantly read from the +network. + +The reason for this change is as simple as *"reading and writing +shouldn't be related"*. Even when you're simply ignoring updates, this +way, once you send a request you will only need to read the result for +the request. Whatever Telegram sent before has already been read and +outside the buffer. + +.. additions-2: + +Additions +--------- + +- The mentioned different connection modes, and a new thread. +- You can modify the ``Session`` attributes through the + ``TelegramClient`` constructor (using ``**kwargs``). +- ``RPCError``\ 's now belong to some request you've made, which makes + more sense. +- ``get_input_*`` now handles ``None`` (default) parameters more + gracefully (it used to crash). + +.. enhancements-4: + +Enhancements +------------ + +- The low-level socket doesn't use a handcrafted timeout anymore, which + should benefit by avoiding the arbitrary ``sleep(0.1)`` that there + used to be. +- ``TelegramClient.sign_in`` will call ``.send_code_request`` if no + ``code`` was provided. + +Deprecation: +------------ + +- ``.sign_up`` does *not* take a ``phone`` argument anymore. Change + this or you will be using ``phone`` as ``code``, and it will fail! + The definition looks like + ``def sign_up(self, code, first_name, last_name='')``. +- The old ``JsonSession`` finally replaces the original ``Session`` + (which used pickle). If you were overriding any of these, you should + only worry about overriding ``Session`` now. + +Added verification for CDN file (v0.12.2) +========================================= + +*Published at 2017/08/28* + +Since the Content Distributed Network (CDN) is not handled by Telegram +itself, the owners may tamper these files. Telegram sends their sha256 +sum for clients to implement this additional verification step, which +now the library has. If any CDN has altered the file you're trying to +download, ``CdnFileTamperedError`` will be raised to let you know. + +Besides this. ``TLObject.stringify()`` was showing bytes as lists (now +fixed) and RPC errors are reported by default: + + In an attempt to help everyone who works with the Telegram API, + Telethon will by default report all Remote Procedure Call errors to + `PWRTelegram `__, a public database anyone can + query, made by `Daniil `__. All the information + sent is a GET request with the error code, error message and method used. + + +.. note:: + + If you still would like to opt out, simply set + ``client.session.report_errors = False`` to disable this feature. + However Daniil would really thank you if you helped him (and everyone) + by keeping it on! + +CDN support (v0.12.1) +===================== + +*Published at 2017/08/24* + +The biggest news for this update are that downloading media from CDN's +(you'll often encounter this when working with popular channels) now +**works**. + +Some bug fixes: + +- The method used to download documents crashed because + two lines were swapped. +- Determining the right path when downloading any file was + very weird, now it's been enhanced. +- The ``.sign_in()`` method didn't support integer values for the code! + Now it does again. + +Some important internal changes are that the old way to deal with RSA +public keys now uses a different module instead the old strange +hand-crafted version. + +Hope the new, super simple ``README.rst`` encourages people to use +Telethon and make it better with either suggestions, or pull request. +Pull requests are *super* appreciated, but showing some support by +leaving a star also feels nice ⭐️. + +Newbie friendly update (v0.12) +============================== + +*Published at 2017/08/22* + ++-----------------------+ +| Scheme layer used: 70 | ++-----------------------+ + +This update is overall an attempt to make Telethon a bit more user +friendly, along with some other stability enhancements, although it +brings quite a few changes. + +Things that will probably break your code +----------------------------------------- + +- The ``TelegramClient`` methods ``.send_photo_file()``, + ``.send_document_file()`` and ``.send_media_file()`` are now a + **single method** called ``.send_file()``. It's also important to + note that the **order** of the parameters has been **swapped**: first + to *who* you want to send it, then the file itself. + +- The same applies to ``.download_msg_media()``, which has been renamed + to ``.download_media()``. The method now supports a ``Message`` + itself too, rather than only ``Message.media``. The specialized + ``.download_photo()``, ``.download_document()`` and + ``.download_contact()`` still exist, but are private. + +More new stuff +-------------- + +- Updated to **layer 70**! +- Both downloading and uploading now support **stream-like objects**. +- A lot **faster initial connection** if ``sympy`` is installed (can be + installed through ``pip``). +- ``libssl`` will also be used if available on your system (likely on + Linux based systems). This speed boost should also apply to uploading + and downloading files. +- You can use a **phone number** or an **username** for methods like + ``.send_message()``, ``.send_file()``, and all the other quick-access + methods provided by the ``TelegramClient``. + +.. bug-fixes-5: + +Bug fixes +--------- + +- Crashing when migrating to a new layer and receiving old updates + should not happen now. +- ``InputPeerChannel`` is now casted to ``InputChannel`` automtically + too. +- ``.get_new_msg_id()`` should now be thread-safe. No promises. +- Logging out on macOS caused a crash, which should be gone now. +- More checks to ensure that the connection is flagged correctly as + either connected or not. + +Bug additions +------------- + +- Downloading files from CDN's will **not work** yet (something new + that comes with layer 70). + +-------------- + +That's it, any new idea or suggestion about how to make the project even +more friendly is highly appreciated. + +.. note:: + + Did you know that you can pretty print any result Telegram returns + (called ``TLObject``\ 's) by using their ``.stringify()`` function? + Great for debugging! + +get_input_* now works with vectors (v0.11.5) +============================================= + +*Published at 2017/07/11* + +Quick fix-up of a bug which hadn't been encountered until now. Auto-cast +by using ``get_input_*`` now works. + +get_input_* everywhere (v0.11.4) +================================= + +*Published at 2017/07/10* + +For some reason, Telegram doesn't have enough with the +`InputPeer `__. +There also exist +`InputChannel `__ +and +`InputUser `__! +You don't have to worry about those anymore, it's handled internally +now. + +Besides this, every Telegram object now features a new default +``.__str__`` look, and also a `.stringify() +method `__ +to pretty format them, if you ever need to inspect them. + +The library now uses `the DEBUG +level `__ +everywhere, so no more warnings or information messages if you had +logging enabled. + +The ``no_webpage`` parameter from ``.send_message`` `has been +renamed `__ +to ``link_preview`` for clarity, so now it does the opposite (but has a +clearer intention). + +Quick .send_message() fix (v0.11.3) +=================================== + +*Published at 2017/07/05* + +A very quick follow-up release to fix a tiny bug with +``.send_message()``, no new features. + +Callable TelegramClient (v0.11.2) +================================= + +*Published at 2017/07/04* + ++-----------------------+ +| Scheme layer used: 68 | ++-----------------------+ + +There is a new preferred way to **invoke requests**, which you're +encouraged to use: + +.. code:: python + + # New! + result = client(SomeRequest()) + + # Old. + result = client.invoke(SomeRequest()) + +Existing code will continue working, since the old ``.invoke()`` has not +been deprecated. + +When you ``.create_new_connection()``, it will also handle +``FileMigrateError``\ 's for you, so you don't need to worry about those +anymore. + +.. bugs-fixed-3: + +Bugs fixed: +----------- + +- Fixed some errors when installing Telethon via ``pip`` (for those + using either source distributions or a Python version ≤ 3.5). +- ``ConnectionResetError`` didn't flag sockets as closed, but now it + does. + +On a more technical side, ``msg_id``\ 's are now more accurate. + +Improvements to the updates (v0.11.1) +===================================== + +*Published at 2017/06/24* + +Receiving new updates shouldn't miss any anymore, also, periodic pings +are back again so it should work on the long run. + +On a different order of things, ``.connect()`` also features a timeout. +Notice that the ``timeout=`` is **not** passed as a **parameter** +anymore, and is instead specified when creating the ``TelegramClient``. + +Some other bug fixes: +- Fixed some name class when a request had a ``.msg_id`` parameter. +- The correct amount of random bytes is now used in DH request +- Fixed ``CONNECTION_APP_VERSION_EMPTY`` when using temporary sessions. +- Avoid connecting if already connected. + +Support for parallel connections (v0.11) +======================================== + +*Published at 2017/06/16* + +*This update brings a lot of changes, so it would be nice if you could* +**read the whole change log**! + +Things that may break your code +------------------------------- + +- Every Telegram error has now its **own class**, so it's easier to + fine-tune your ``except``\ 's. +- Markdown parsing is **not part** of Telethon itself anymore, although + there are plans to support it again through a some external module. +- The ``.list_sessions()`` has been moved to the ``Session`` class + instead. +- The ``InteractiveTelegramClient`` is **not** shipped with ``pip`` + anymore. + +New features +------------ + +- A new, more **lightweight class** has been added. The + ``TelegramBareClient`` is now the base of the normal + ``TelegramClient``, and has the most basic features. +- New method to ``.create_new_connection()``, which can be ran **in + parallel** with the original connection. This will return the + previously mentioned ``TelegramBareClient`` already connected. +- Any file object can now be used to download a file (for instance, a + ``BytesIO()`` instead a file name). +- Vales like ``random_id`` are now **automatically inferred**, so you + can save yourself from the hassle of writing + ``generate_random_long()`` everywhere. Same applies to + ``.get_input_peer()``, unless you really need the extra performance + provided by skipping one ``if`` if called manually. +- Every type now features a new ``.to_dict()`` method. + +.. bug-fixes-6: + +Bug fixes +--------- + +- Received errors are acknowledged to the server, so they don't happen + over and over. +- Downloading media on different data centers is now up to **x2 + faster**, since there used to be an ``InvalidDCError`` for each file + part tried to be downloaded. +- Lost messages are now properly skipped. +- New way to handle the **result of requests**. The old ``ValueError`` + "*The previously sent request must be resent. However, no request was + previously sent (possibly called from a different thread).*" *should* + not happen anymore. + +Minor highlights +---------------- + +- Some fixes to the ``JsonSession``. +- Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while + ``.reconnect()`` was being called on the ``UpdatesThread``. +- Some improvements on the ``TcpClient``, such as not switching between + blocking and non-blocking sockets. +- The code now uses ASCII characters only. +- Some enhancements to ``.find_user_or_chat()`` and + ``.get_input_peer()``. + +JSON session file (v0.10.1) +=========================== + +*Published at 2017/06/07* + +This version is primarily for people to **migrate** their ``.session`` +files, which are *pickled*, to the new *JSON* format. Although slightly +slower, and a bit more vulnerable since it's plain text, it's a lot more +resistant to upgrades. + +.. warning:: + + You **must** upgrade to this version before any higher one if you've + used Telethon ≤ v0.10. If you happen to upgrade to an higher version, + that's okay, but you will have to manually delete the ``*.session`` file, + and logout from that session from an official client. + +Other highlights: + +- New ``.get_me()`` function to get the **current** user. +- ``.is_user_authorized()`` is now more reliable. +- New nice button to copy the ``from telethon.tl.xxx.yyy import Yyy`` + on the online documentation. +- Everything on the documentation is now, theoretically, **sorted + alphabetically**. +- **More error codes** added to the ``errors`` file. +- No second thread is spawned unless one or more update handlers are added. + +Full support for different DCs and ++stable (v0.10) +=================================================== + +*Published at 2017/06/03* + +Working with **different data centers** finally *works*! On a different +order of things, **reconnection** is now performed automatically every +time Telegram decides to kick us off their servers, so now Telethon can +really run **forever and ever**! In theory. + +Another important highlights: + +- **Documentation** improvements, such as showing the return type. +- The ``msg_id too low/high`` error should happen **less often**, if + any. +- Sleeping on the main thread is **not done anymore**. You will have to + ``except FloodWaitError``\ 's. +- You can now specify your *own application version*, device model, + system version and language code. +- Code is now more *pythonic* (such as making some members private), + and other internal improvements (which affect the **updates + thread**), such as using ``logger`` instead a bare ``print()`` too. + +This brings Telethon a whole step closer to ``v1.0``, though more things +should preferably be changed. + +Stability improvements (v0.9.1) +=============================== + +*Published at 2017/05/23* + +Telethon used to crash a lot when logging in for the very first time. +The reason for this was that the reconnection (or dead connections) were +not handled properly. Now they are, so you should be able to login +directly, without needing to delete the ``*.session`` file anymore. +Notice that downloading from a different DC is still a WIP. + +Some highlights: + +- Updates thread is only started after a successful login. +- Files meant to be ran by the user now use **shebangs** and + proper permissions. +- In-code documentation now shows the returning type. +- **Relative import** is now used everywhere, so you can rename + ``telethon`` to anything else. +- **Dead connections** are now **detected** instead entering an infinite loop. +- **Sockets** can now be **closed** (and re-opened) properly. +- Telegram decided to update the layer 66 without increasing the number. + This has been fixed and now we're up-to-date again. + +General improvements (v0.9) +=========================== + +*Published at 2017/05/19* + ++-----------------------+ +| Scheme layer used: 66 | ++-----------------------+ + +This release features: + +- The **documentation**, available online + `here `__, has a new search bar. +- Better **cross-thread safety** by using ``threading.Event``. +- More improvements for running Telethon during a **long period of time**. + +With the following bug fixes: + +- **Avoid a certain crash on login** (occurred if an unexpected object + ID was received). +- Avoid crashing with certain invalid UTF-8 strings. +- Avoid crashing on certain terminals by using known ASCII characters + where possible. +- The ``UpdatesThread`` is now a daemon, and should cause less issues. +- Temporary sessions didn't actually work (with ``session=None``). + +Minor notes: + +- ``.get_dialogs(count=`` was renamed to ``.get_dialogs(limit=``. + +Bot login and proxy support (v0.8) +================================== + +*Published at 2017/04/14* + +This release features: + +- **Bot login**, thanks to @JuanPotato for hinting me about how to do + it. +- **Proxy support**, thanks to @exzhawk for implementing it. +- **Logging support**, used by passing ``--telethon-log=DEBUG`` (or + ``INFO``) as a command line argument. + +With the following bug fixes: + +- Connection fixes, such as avoiding connection until ``.connect()`` is + explicitly invoked. +- Uploading big files now works correctly. +- Fix uploading big files. +- Some fixes on the updates thread, such as correctly sleeping when required. + +Long-run bug fix (v0.7.1) +========================= + +*Published at 2017/02/19* + +If you're one of those who runs Telethon for a long time (more than 30 +minutes), this update by @strayge will be great for you. It sends +periodic pings to the Telegram servers so you don't get disconnected and +you can still send and receive updates! + +Two factor authentication (v0.7) +================================ + +*Published at 2017/01/31* + ++-----------------------+ +| Scheme layer used: 62 | ++-----------------------+ + +If you're one of those who love security the most, these are good news. +You can now use two factor authentication with Telethon too! As internal +changes, the coding style has been improved, and you can easily use +custom session objects, and various little bugs have been fixed. + +Updated pip version (v0.6) +========================== + +*Published at 2016/11/13* + ++-----------------------+ +| Scheme layer used: 57 | ++-----------------------+ + +This release has no new major features. However, it contains some small +changes that make using Telethon a little bit easier. Now those who have +installed Telethon via ``pip`` can also take advantage of changes, such +as less bugs, creating empty instances of ``TLObjects``, specifying a +timeout and more! + +Ready, pip, go! (v0.5) +====================== + +*Published at 2016/09/18* + +Telethon is now available as a **`Python +package `__**! Those are +really exciting news (except, sadly, the project structure had to change +*a lot* to be able to do that; but hopefully it won't need to change +much more, any more!) + +Not only that, but more improvements have also been made: you're now +able to both **sign up** and **logout**, watch a pretty +"Uploading/Downloading… x%" progress, and other minor changes which make +using Telethon **easier**. + +Made InteractiveTelegramClient cool (v0.4) +========================================== + +*Published at 2016/09/12* + +Yes, really cool! I promise. Even though this is meant to be a +*library*, that doesn't mean it can't have a good *interactive client* +for you to try the library out. This is why now you can do many, many +things with the ``InteractiveTelegramClient``: + +- **List dialogs** (chats) and pick any you wish. +- **Send any message** you like, text, photos or even documents. +- **List** the **latest messages** in the chat. +- **Download** any message's media (photos, documents or even contacts!). +- **Receive message updates** as you talk (i.e., someone sent you a message). + +It actually is an usable-enough client for your day by day. You could +even add ``libnotify`` and pop, you're done! A great cli-client with +desktop notifications. + +Also, being able to download and upload media implies that you can do +the same with the library itself. Did I need to mention that? Oh, and +now, with even less bugs! I hope. + +Media revolution and improvements to update handling! (v0.3) +============================================================ + +*Published at 2016/09/11* + +Telegram is more than an application to send and receive messages. You +can also **send and receive media**. Now, this implementation also gives +you the power to upload and download media from any message that +contains it! Nothing can now stop you from filling up all your disk +space with all the photos! If you want to, of course. + +Handle updates in their own thread! (v0.2) +========================================== + +*Published at 2016/09/10* + +This version handles **updates in a different thread** (if you wish to +do so). This means that both the low level ``TcpClient`` and the +not-so-low-level ``MtProtoSender`` are now multi-thread safe, so you can +use them with more than a single thread without worrying! + +This also implies that you won't need to send a request to **receive an +update** (is someone typing? did they send me a message? has someone +gone offline?). They will all be received **instantly**. + +Some other cool examples of things that you can do: when someone tells +you "*Hello*", you can automatically reply with another "*Hello*" +without even needing to type it by yourself :) + +However, be careful with spamming!! Do **not** use the program for that! + +First working alpha version! (v0.1) +=================================== + +*Published at 2016/09/06* + ++-----------------------+ +| Scheme layer used: 55 | ++-----------------------+ + +There probably are some bugs left, which haven't yet been found. +However, the majority of code works and the application is already +usable! Not only that, but also uses the latest scheme as of now *and* +handles way better the errors. This tag is being used to mark this +release as stable enough. diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 8e5c6053..161c4b1a 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -10,7 +10,8 @@ Welcome to Telethon's documentation! Pure Python 3 Telegram client library. Official Site `here `_. -Please follow the links below to get you started. +Please follow the links below to get you started, and remember +to read the :ref:`changelog` when you upgrade! .. _installation-and-usage: From c039ba3e16bedfe7eaea87a8cac7c87dac3fc5d0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 23:48:21 +0100 Subject: [PATCH 059/361] Be consistent with the titles in the changelog --- readthedocs/extra/changelog.rst | 242 ++++++++++++++++++-------------- 1 file changed, 138 insertions(+), 104 deletions(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 96ab594c..569f21ca 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -49,7 +49,7 @@ Breaking changes Both lists have a ``.total`` attribute so you can still know how many dialogs/messages are in total. -New stuff +Additions ~~~~~~~~~ - The mentioned use of ``sqlite3`` for the session file. @@ -183,8 +183,8 @@ Bug fixes - Code generation was using f-strings, which are only supported on Python ≥3.6. -Other changes -~~~~~~~~~~~~~ +Internal changes +~~~~~~~~~~~~~~~~ - The ``auth_key`` generation has been moved from ``.connect()`` to ``.invoke()``. There were some issues were ``.connect()`` failed and @@ -227,25 +227,8 @@ Bug fixes and new small features (v0.15.2) This release primarly focuses on a few bug fixes and enhancements. Although more stuff may have broken along the way. -.. bug-fixes-3: - -Bug fixes: -~~~~~~~~~~ - -- ``.get_input_entity`` was failing for IDs and other cases, also - making more requests than it should. -- Use ``basename`` instead ``abspath`` when sending a file. You can now - also override the attributes. -- ``EntityDatabase.__delitem__`` wasn't working. -- ``.send_message()`` was failing with channels. -- ``.get_dialogs(limit=None)`` should now return all the dialogs - correctly. -- Temporary fix for abusive duplicated updates. - -.. enhancements-1: - -Enhancements: -~~~~~~~~~~~~~ +Enhancements +~~~~~~~~~~~~ - You will be warned if you call ``.add_update_handler`` with no ``update_workers``. @@ -260,10 +243,27 @@ Enhancements: - You can use a callable key for the ``EntityDatabase``, so it can be any filter you need. +.. bug-fixes-3: + +Bug fixes +~~~~~~~~~ + +- ``.get_input_entity`` was failing for IDs and other cases, also + making more requests than it should. +- Use ``basename`` instead ``abspath`` when sending a file. You can now + also override the attributes. +- ``EntityDatabase.__delitem__`` wasn't working. +- ``.send_message()`` was failing with channels. +- ``.get_dialogs(limit=None)`` should now return all the dialogs + correctly. +- Temporary fix for abusive duplicated updates. + +.. enhancements-1: + .. internal-changes-1: -Internal changes: -~~~~~~~~~~~~~~~~~ +Internal changes +~~~~~~~~~~~~~~~~ - MsgsAck is now sent in a container rather than its own request. - ``.get_input_photo`` is now used in the generated code. @@ -295,16 +295,26 @@ for a certain user through ``client.session.entities.clear_cache(entity=None)``, which will clear all if no entity is given. -More things: -- ``.sign_in`` accepts phones as integers. -- ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the - right amount of dialogs. +Additions +~~~~~~~~~ + - New method to ``.delete_messages()``. - New ``ChannelPrivateError`` class. + +Enhancements +~~~~~~~~~~~~ + +- ``.sign_in`` accepts phones as integers. - Changing the IP to which you connect to is as simple as ``client.session.server_address = 'ip'``, since now the server address is always queried from the session. + +Bug fixes +~~~~~~~~~ + +- ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the + right amount of dialogs. - ``GeneralProxyError`` should be passed to the main thread again, so that you can handle it. @@ -316,8 +326,15 @@ Updates Overhaul Update (v0.15) After hundreds of lines changed on a major refactor, *it's finally here*. It's the **Updates Overhaul Update**; let's get right into it! -New stuff and enhancements -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Breaking changes +~~~~~~~~~~~~~~~~ + +- ``.create_new_connection()`` is gone for good. No need to deal with + this manually since new connections are now handled on demand by the + library itself. + +Enhancements +~~~~~~~~~~~~ - You can **invoke** requests from **update handlers**. And **any other thread**. A new temporary will be made, so that you can be sending @@ -338,10 +355,6 @@ New stuff and enhancements - New ``.get_input_entity`` method, which makes use of the above feature. You **should use this** when a request needs a ``InputPeer``, rather than the whole entity (although both work). - -Less important enhancements -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Assert that either all or None dependent-flag parameters are set before sending the request. - Phone numbers can have dashes, spaces, or parenthesis. They'll be @@ -349,15 +362,8 @@ Less important enhancements - You can override the phone and its hash on ``.sign_in()``, if you're creating a new ``TelegramClient`` on two different places. -Compatibility breaks -~~~~~~~~~~~~~~~~~~~~ - -- ``.create_new_connection()`` is gone for good. No need to deal with - this manually since new connections are now handled on demand by the - library itself. - -Bugs fixed -~~~~~~~~~~ +Bug fixes +~~~~~~~~~ - ``.log_out()`` was consuming all retries. It should work just fine now. @@ -403,18 +409,22 @@ Serialization bug fixes (v0.14.2) *Published at 2017/09/29* -Two bug fixes, one of them quite **important**, related to the -serialization. Every object or request that had to serialize a -``True/False`` type was always being serialized as ``false``! +Bug fixes +~~~~~~~~~ -Another bug that didn't allow you to leave as ``None`` flag parameters -that needed a list has been fixed. +- **Important**, related to the serialization. Every object or request + that had to serialize a ``True/False`` type was always being serialized + as ``false``! +- Another bug that didn't allow you to leave as ``None`` flag parameters + that needed a list has been fixed. -Other internal changes include a somewhat more readable ``.to_bytes()`` -function and pre-computing the flag instead using bit shifting. The -``TLObject.constructor_id`` has been renamed to -``TLObject.CONSTRUCTOR_ID``, and ``.subclass_of_id`` is also uppercase -now. +Internal changes +~~~~~~~~~~~~~~~~ + +- Other internal changes include a somewhat more readable ``.to_bytes()`` + function and pre-computing the flag instead using bit shifting. The + ``TLObject.constructor_id`` has been renamed to ``TLObject.CONSTRUCTOR_ID``, + and ``.subclass_of_id`` is also uppercase now. Farewell, BinaryWriter (v0.14.1) ================================ @@ -427,13 +437,18 @@ serializing TLObjects, and this release finally removes it. The speed up when serializing things to bytes should now be over twice as fast wherever it's needed. -Other internal changes include using proper classes (including the -generated code) for generating authorization keys and to write out -``TLMessage``\ 's. +Bug fixes +~~~~~~~~~ + +- This version is again compatible with Python 3.x versions **below 3.5** + (there was a method call that was Python 3.5 and above). + +Internal changes +~~~~~~~~~~~~~~~~ + +- Using proper classes (including the generated code) for generating + authorization keys and to write out ``TLMessage``\ 's. -For **bug fixes**, this version is again compatible with Python 3.x -versions **below 3.5** (there was a method call that was Python 3.5 and -above). Several requests at once and upload compression (v0.14) ======================================================= @@ -443,6 +458,9 @@ Several requests at once and upload compression (v0.14) New major release, since I've decided that these two features are big enough: +Additions +~~~~~~~~~ + - Requests larger than 512 bytes will be **compressed through gzip**, and if the result is smaller, this will be uploaded instead. - You can now send **multiple requests at once**, they're simply @@ -454,15 +472,20 @@ Internally, another important change. The ``.on_send`` function on the my tests, this has always been over twice as fast serializing objects, although more replacements need to be done, so please report any issues. -Besides this: +Enhancements +~~~~~~~~~~~~ +- Implemented ``.get_input_media`` helper methods. Now you can even use + another message as input media! + + +Bug fixes +~~~~~~~~~ - Downloading media from CDNs wasn't working (wrong access to a parameter). - Correct type hinting. - Added a tiny sleep when trying to perform automatic reconnection. - Error reporting is done in the background, and has a shorter timeout. -- Implemented ``.get_input_media`` helper methods. Now you can even use - another message as input media! - ``setup.py`` used to fail with wrongly generated code. Quick fix-up (v0.13.6) @@ -529,8 +552,8 @@ Enhancements .. other-changes-1: -Other changes -~~~~~~~~~~~~~ +Internal changes +~~~~~~~~~~~~~~~~ - ``TLObject.__repr__`` doesn't show the original TL definition anymore, it was a lot of clutter. If you have any complaints open an @@ -547,8 +570,8 @@ More bug fixes and enhancements (v0.13.4) .. new-stuff-1: -New stuff: -~~~~~~~~~~ +Additions +~~~~~~~~~ - ``TelegramClient`` now exposes a ``.is_connected()`` method. - Initial authorization on a new data center will retry up to 5 times @@ -558,18 +581,14 @@ New stuff: .. bugs-fixed-1: -Bugs fixed: -~~~~~~~~~~~ +Bug fixes +~~~~~~~~~~ - Now you should be able to sign in even if you have ``process_updates=True`` and no previous session. - Some errors and methods are documented a bit clearer. - ``.send_message()`` could randomly fail, as the returned type was not expected. - -Things that should reduce the amount of crashes: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - ``TimeoutError`` is now ignored, since the request will be retried up to 5 times by default. - "-404" errors (``BrokenAuthKeyError``\ 's) are now detected when @@ -579,8 +598,8 @@ Things that should reduce the amount of crashes: - Attempt at fixing some "NoneType has no attribute…" errors (with the ``.sender``). -Other internal changes: -~~~~~~~~~~~~~~~~~~~~~~~ +Internal changes +~~~~~~~~~~~~~~~~ - Calling ``GetConfigRequest`` is now made less often. - The ``initial_query`` parameter from ``.connect()`` is gone, as it's @@ -596,8 +615,8 @@ Bug fixes and enhancements (v0.13.3) .. bugs-fixed-2: -Bugs fixed ----------- +Bug fixes +--------- - **Reconnection** used to fail because it tried invoking things from the ``ReadThread``. @@ -640,7 +659,8 @@ an in-depth description on how to work with updates now. Notice that you cannot invoke requests from within handlers anymore, only the ``v.0.13.1`` patch allowed you to do so. -**Other fixes**: +Bug fixes +~~~~~~~~~ - Periodic pings are back. - The username regex mentioned on ``UsernameInvalidError`` was invalid, @@ -723,8 +743,8 @@ Enhancements - ``TelegramClient.sign_in`` will call ``.send_code_request`` if no ``code`` was provided. -Deprecation: ------------- +Deprecation +----------- - ``.sign_up`` does *not* take a ``phone`` argument anymore. Change this or you will be using ``phone`` as ``code``, and it will fail! @@ -771,7 +791,8 @@ The biggest news for this update are that downloading media from CDN's (you'll often encounter this when working with popular channels) now **works**. -Some bug fixes: +Bug fixes +~~~~~~~~~ - The method used to download documents crashed because two lines were swapped. @@ -802,8 +823,8 @@ This update is overall an attempt to make Telethon a bit more user friendly, along with some other stability enhancements, although it brings quite a few changes. -Things that will probably break your code ------------------------------------------ +Breaking changes +---------------- - The ``TelegramClient`` methods ``.send_photo_file()``, ``.send_document_file()`` and ``.send_media_file()`` are now a @@ -817,8 +838,8 @@ Things that will probably break your code ``.download_photo()``, ``.download_document()`` and ``.download_contact()`` still exist, but are private. -More new stuff --------------- +Additions +--------- - Updated to **layer 70**! - Both downloading and uploading now support **stream-like objects**. @@ -845,10 +866,9 @@ Bug fixes - More checks to ensure that the connection is flagged correctly as either connected or not. -Bug additions -------------- +.. note:: -- Downloading files from CDN's will **not work** yet (something new + Downloading files from CDN's will **not work** yet (something new that comes with layer 70). -------------- @@ -936,8 +956,8 @@ anymore. .. bugs-fixed-3: -Bugs fixed: ------------ +Bugs fixes +~~~~~~~~~~ - Fixed some errors when installing Telethon via ``pip`` (for those using either source distributions or a Python version ≤ 3.5). @@ -958,7 +978,9 @@ On a different order of things, ``.connect()`` also features a timeout. Notice that the ``timeout=`` is **not** passed as a **parameter** anymore, and is instead specified when creating the ``TelegramClient``. -Some other bug fixes: +Bug fixes +~~~~~~~~~ + - Fixed some name class when a request had a ``.msg_id`` parameter. - The correct amount of random bytes is now used in DH request - Fixed ``CONNECTION_APP_VERSION_EMPTY`` when using temporary sessions. @@ -972,8 +994,8 @@ Support for parallel connections (v0.11) *This update brings a lot of changes, so it would be nice if you could* **read the whole change log**! -Things that may break your code -------------------------------- +Breaking changes +---------------- - Every Telegram error has now its **own class**, so it's easier to fine-tune your ``except``\ 's. @@ -984,8 +1006,8 @@ Things that may break your code - The ``InteractiveTelegramClient`` is **not** shipped with ``pip`` anymore. -New features ------------- +Additions +--------- - A new, more **lightweight class** has been added. The ``TelegramBareClient`` is now the base of the normal @@ -1018,7 +1040,7 @@ Bug fixes previously sent (possibly called from a different thread).*" *should* not happen anymore. -Minor highlights +Internal changes ---------------- - Some fixes to the ``JsonSession``. @@ -1047,15 +1069,20 @@ resistant to upgrades. that's okay, but you will have to manually delete the ``*.session`` file, and logout from that session from an official client. -Other highlights: +Additions +~~~~~~~~~ - New ``.get_me()`` function to get the **current** user. - ``.is_user_authorized()`` is now more reliable. - New nice button to copy the ``from telethon.tl.xxx.yyy import Yyy`` on the online documentation. +- **More error codes** added to the ``errors`` file. + +Enhancements +~~~~~~~~~~~~ + - Everything on the documentation is now, theoretically, **sorted alphabetically**. -- **More error codes** added to the ``errors`` file. - No second thread is spawned unless one or more update handlers are added. Full support for different DCs and ++stable (v0.10) @@ -1068,7 +1095,8 @@ order of things, **reconnection** is now performed automatically every time Telegram decides to kick us off their servers, so now Telethon can really run **forever and ever**! In theory. -Another important highlights: +Enhancements +~~~~~~~~~~~~ - **Documentation** improvements, such as showing the return type. - The ``msg_id too low/high`` error should happen **less often**, if @@ -1095,7 +1123,8 @@ not handled properly. Now they are, so you should be able to login directly, without needing to delete the ``*.session`` file anymore. Notice that downloading from a different DC is still a WIP. -Some highlights: +Enhancements +~~~~~~~~~~~~ - Updates thread is only started after a successful login. - Files meant to be ran by the user now use **shebangs** and @@ -1117,14 +1146,16 @@ General improvements (v0.9) | Scheme layer used: 66 | +-----------------------+ -This release features: +Additions +~~~~~~~~~ - The **documentation**, available online `here `__, has a new search bar. - Better **cross-thread safety** by using ``threading.Event``. - More improvements for running Telethon during a **long period of time**. -With the following bug fixes: +Bug fixes +~~~~~~~~~ - **Avoid a certain crash on login** (occurred if an unexpected object ID was received). @@ -1134,7 +1165,8 @@ With the following bug fixes: - The ``UpdatesThread`` is now a daemon, and should cause less issues. - Temporary sessions didn't actually work (with ``session=None``). -Minor notes: +Internal changes +~~~~~~~~~~~~~~~~ - ``.get_dialogs(count=`` was renamed to ``.get_dialogs(limit=``. @@ -1143,7 +1175,8 @@ Bot login and proxy support (v0.8) *Published at 2017/04/14* -This release features: +Additions +~~~~~~~~~ - **Bot login**, thanks to @JuanPotato for hinting me about how to do it. @@ -1151,7 +1184,8 @@ This release features: - **Logging support**, used by passing ``--telethon-log=DEBUG`` (or ``INFO``) as a command line argument. -With the following bug fixes: +Bug fixes +~~~~~~~~~ - Connection fixes, such as avoiding connection until ``.connect()`` is explicitly invoked. From 3eafe18d0b8bf535c140e845eae2e3a0be78701c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 6 Jan 2018 01:55:11 +0100 Subject: [PATCH 060/361] Implement MtProto 2.0 (closes #484, thanks @delivrance!) Huge shoutout to @delivrance's pyrogram, specially this commit: pyrogram/pyrogram/commit/42f9a2d6994baaf9ecad590d1ff4d175a8c56454 --- telethon/extensions/binary_reader.py | 5 ++- telethon/helpers.py | 67 ++++++++++++++++++++++++++-- telethon/network/mtproto_sender.py | 44 +++--------------- 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 460bed96..1402083f 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -56,8 +56,11 @@ class BinaryReader: return int.from_bytes( self.read(bits // 8), byteorder='little', signed=signed) - def read(self, length): + def read(self, length=None): """Read the given amount of bytes.""" + if length is None: + return self.reader.read() + result = self.reader.read(length) if len(result) != length: raise BufferError( diff --git a/telethon/helpers.py b/telethon/helpers.py index 3c9af2cb..d97b8a9f 100644 --- a/telethon/helpers.py +++ b/telethon/helpers.py @@ -1,6 +1,11 @@ """Various helpers not related to the Telegram API itself""" -from hashlib import sha1, sha256 import os +import struct +from hashlib import sha1, sha256 + +from telethon.crypto import AES +from telethon.extensions import BinaryReader + # region Multiple utilities @@ -21,9 +26,48 @@ def ensure_parent_dir_exists(file_path): # region Cryptographic related utils +def pack_message(session, message): + """Packs a message following MtProto 2.0 guidelines""" + # See https://core.telegram.org/mtproto/description + data = struct.pack(' Date: Sat, 6 Jan 2018 02:03:23 +0100 Subject: [PATCH 061/361] Add a few security checks when unpacking messages from server Also delete MtProto 1.0 leftovers. --- telethon/helpers.py | 40 +++++++++++++---------------------- telethon_tests/crypto_test.py | 1 + 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/telethon/helpers.py b/telethon/helpers.py index d97b8a9f..82b551ab 100644 --- a/telethon/helpers.py +++ b/telethon/helpers.py @@ -4,6 +4,7 @@ import struct from hashlib import sha1, sha256 from telethon.crypto import AES +from telethon.errors import SecurityError from telethon.extensions import BinaryReader @@ -39,7 +40,7 @@ def pack_message(session, message): # "msg_key = substr (msg_key_large, 8, 16)" msg_key = msg_key_large[8:24] - aes_key, aes_iv = calc_key_2(session.auth_key.key, msg_key, True) + aes_key, aes_iv = calc_key(session.auth_key.key, msg_key, True) key_id = struct.pack(' Date: Sat, 6 Jan 2018 13:37:46 +0100 Subject: [PATCH 062/361] Fix a few more issue styles with RTD (mostly lists/nested md) --- .../telegram-api-in-other-languages.rst | 20 ++++++++-------- .../extra/troubleshooting/rpc-errors.rst | 13 +++++----- readthedocs/extra/wall-of-shame.rst | 24 ++++++++++--------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/readthedocs/extra/developing/telegram-api-in-other-languages.rst b/readthedocs/extra/developing/telegram-api-in-other-languages.rst index 0adeb988..44e45d51 100644 --- a/readthedocs/extra/developing/telegram-api-in-other-languages.rst +++ b/readthedocs/extra/developing/telegram-api-in-other-languages.rst @@ -13,18 +13,18 @@ C * Possibly the most well-known unofficial open source implementation out -there by `**@vysheng** `__, -```tgl`` `__, and its console client -```telegram-cli`` `__. Latest development +there by `@vysheng `__, +`tgl `__, and its console client +`telegram-cli `__. Latest development has been moved to `BitBucket `__. JavaScript ********** -`**@zerobias** `__ is working on -```telegram-mtproto`` `__, +`@zerobias `__ is working on +`telegram-mtproto `__, a work-in-progress JavaScript library installable via -```npm`` `__. +`npm `__. Kotlin ****** @@ -34,14 +34,14 @@ implementation written in Kotlin (the now `official `__ language for `Android `__) by -`**@badoualy** `__, currently as a beta– +`@badoualy `__, currently as a beta– yet working. PHP *** A PHP implementation is also available thanks to -`**@danog** `__ and his +`@danog `__ and his `MadelineProto `__ project, with a very nice `online documentation `__ too. @@ -51,7 +51,7 @@ Python A fairly new (as of the end of 2017) Telegram library written from the ground up in Python by -`**@delivrance** `__ and his +`@delivrance `__ and his `Pyrogram `__ library! No hard feelings Dan and good luck dealing with some of your users ;) @@ -59,6 +59,6 @@ Rust **** Yet another work-in-progress implementation, this time for Rust thanks -to `**@JuanPotato** `__ under the fancy +to `@JuanPotato `__ under the fancy name of `Vail `__. This one is very early still, but progress is being made at a steady rate. diff --git a/readthedocs/extra/troubleshooting/rpc-errors.rst b/readthedocs/extra/troubleshooting/rpc-errors.rst index 55a21d7b..0d36bec6 100644 --- a/readthedocs/extra/troubleshooting/rpc-errors.rst +++ b/readthedocs/extra/troubleshooting/rpc-errors.rst @@ -17,11 +17,12 @@ something went wrong on Telegram's server). The most common are: said operation on a chat or channel. Try avoiding filters, i.e. when searching messages. -The generic classes for different error codes are: \* ``InvalidDCError`` -(303), the request must be repeated on another DC. \* -``BadRequestError`` (400), the request contained errors. \* -``UnauthorizedError`` (401), the user is not authorized yet. \* -``ForbiddenError`` (403), privacy violation error. \* ``NotFoundError`` -(404), make sure you're invoking ``Request``\ 's! +The generic classes for different error codes are: + +- ``InvalidDCError`` (303), the request must be repeated on another DC. +- ``BadRequestError`` (400), the request contained errors. +- ``UnauthorizedError`` (401), the user is not authorized yet. +- ``ForbiddenError`` (403), privacy violation error. +- ``NotFoundError`` (404), make sure you're invoking ``Request``\ 's! If the error is not recognised, it will only be an ``RPCError``. diff --git a/readthedocs/extra/wall-of-shame.rst b/readthedocs/extra/wall-of-shame.rst index 95ad3e04..dfede312 100644 --- a/readthedocs/extra/wall-of-shame.rst +++ b/readthedocs/extra/wall-of-shame.rst @@ -17,17 +17,19 @@ Shame `__: -> > **rtfm** -> > Literally "Read The F\ **king Manual"; a term showing the -frustration of being bothered with questions so trivial that the asker -could have quickly figured out the answer on their own with minimal -effort, usually by reading readily-available documents. People who -say"RTFM!" might be considered rude, but the true rude ones are the -annoying people who take absolutely no self-responibility and expect to -have all the answers handed to them personally. -> > *"Damn, that's the twelveth time that somebody posted this question -to the messageboard today! RTFM, already!"* -> > **\ by Bill M. July 27, 2004*\* + **rtfm** + Literally "Read The F--king Manual"; a term showing the + frustration of being bothered with questions so trivial that the asker + could have quickly figured out the answer on their own with minimal + effort, usually by reading readily-available documents. People who + say"RTFM!" might be considered rude, but the true rude ones are the + annoying people who take absolutely no self-responibility and expect to + have all the answers handed to them personally. + + *"Damn, that's the twelveth time that somebody posted this question + to the messageboard today! RTFM, already!"* + + *by Bill M. July 27, 2004* If you have indeed read the wiki, and have tried looking for the method, and yet you didn't find what you need, **that's fine**. Telegram's API From f357d00911ccc720857077e2c16c8046b6a869b7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 6 Jan 2018 15:54:27 +0100 Subject: [PATCH 063/361] Assert user/channel ID is non-zero too for #392 --- telethon/tl/session.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 59794f16..c7f72c0c 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -323,12 +323,19 @@ class Session: except ValueError: continue - p_hash = getattr(p, 'access_hash', 0) - if p_hash is None: - # Some users and channels seem to be returned without - # an 'access_hash', meaning Telegram doesn't want you - # to access them. This is the reason behind ensuring - # that the 'access_hash' is non-zero. See issue #354. + if isinstance(p, (InputPeerUser, InputPeerChannel)): + if not p.access_hash: + # Some users and channels seem to be returned without + # an 'access_hash', meaning Telegram doesn't want you + # to access them. This is the reason behind ensuring + # that the 'access_hash' is non-zero. See issue #354. + # Note that this checks for zero or None, see #392. + continue + else: + p_hash = p.access_hash + elif isinstance(p, InputPeerChat): + p_hash = 0 + else: continue username = getattr(e, 'username', None) or None From 7745b8e7eeb73b103b577a2c7890e456982eaab5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 6 Jan 2018 19:35:24 +0100 Subject: [PATCH 064/361] Use without rowid only if supported (closes #523) --- telethon/tl/session.py | 58 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index c7f72c0c..930b6973 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -107,36 +107,34 @@ class Session: c.close() else: # Tables don't exist, create new ones - c.execute("create table version (version integer)") - c.execute("insert into version values (?)", (CURRENT_VERSION,)) - c.execute( - """create table sessions ( + self._create_table( + c, + "version (version integer primary key)" + , + """sessions ( dc_id integer primary key, server_address text, port integer, auth_key blob - ) without rowid""" - ) - c.execute( - """create table entities ( + )""" + , + """entities ( id integer primary key, hash integer not null, username text, phone integer, name text - ) without rowid""" - ) - # Save file_size along with md5_digest - # to make collisions even more unlikely. - c.execute( - """create table sent_files ( + )""" + , + """sent_files ( md5_digest blob, file_size integer, file_id integer, part_count integer, primary key(md5_digest, file_size) - ) without rowid""" + )""" ) + c.execute("insert into version values (?)", (CURRENT_VERSION,)) # Migrating from JSON -> new table and may have entities if entities: c.executemany( @@ -170,17 +168,29 @@ class Session: return [] # No entities def _upgrade_database(self, old): + c = self._conn.cursor() if old == 1: - self._conn.execute( - """create table sent_files ( - md5_digest blob, - file_size integer, - file_id integer, - part_count integer, - primary key(md5_digest, file_size) - ) without rowid""" - ) + self._create_table(c,"""sent_files ( + md5_digest blob, + file_size integer, + file_id integer, + part_count integer, + primary key(md5_digest, file_size) + )""") old = 2 + c.close() + + def _create_table(self, c, *definitions): + """ + Creates a table given its definition 'name (columns). + If the sqlite version is >= 3.8.2, it will use "without rowid". + See http://www.sqlite.org/releaselog/3_8_2.html. + """ + required = (3, 8, 2) + sqlite_v = tuple(int(x) for x in sqlite3.sqlite_version.split('.')) + extra = ' without rowid' if sqlite_v >= required else '' + for definition in definitions: + c.execute('create table {}{}'.format(definition, extra)) # Data from sessions should be kept as properties # not to fetch the database every time we need it From d81dd055e6d54c23f093a52151110b49f7552e64 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 6 Jan 2018 23:43:40 +0100 Subject: [PATCH 065/361] Remove temporary connections and use a lock again These seem to be the reason for missing some updates (#237) --- telethon/network/mtproto_sender.py | 11 ++-- telethon/telegram_bare_client.py | 88 +++++++++--------------------- 2 files changed, 31 insertions(+), 68 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 82a378ba..0e960181 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -5,6 +5,7 @@ encrypting every packet, and relies on a valid AuthKey in the used Session. import gzip import logging import struct +from threading import Lock from .. import helpers as utils from ..crypto import AES @@ -53,6 +54,9 @@ class MtProtoSender: # Requests (as msg_id: Message) sent waiting to be received self._pending_receive = {} + # Multithreading + self._send_lock = Lock() + def connect(self): """Connects to the server.""" self.connection.connect(self.session.server_address, self.session.port) @@ -71,10 +75,6 @@ class MtProtoSender: self._need_confirmation.clear() self._clear_all_pending() - def clone(self): - """Creates a copy of this MtProtoSender as a new connection.""" - return MtProtoSender(self.session, self.connection.clone()) - # region Send and receive def send(self, *requests): @@ -156,7 +156,8 @@ class MtProtoSender: :param message: the TLMessage to be sent. """ - self.connection.send(utils.pack_message(self.session, message)) + with self._send_lock: + self.connection.send(utils.pack_message(self.session, message)) def _decode_msg(self, body): """ diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index ab6d3bbb..429a4306 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -163,11 +163,6 @@ class TelegramBareClient: self._spawn_read_thread = spawn_read_thread self._recv_thread = None - # Identifier of the main thread (the one that called .connect()). - # This will be used to create new connections from any other thread, - # so that requests can be sent in parallel. - self._main_thread_ident = None - # Default PingRequest delay self._last_ping = datetime.now() self._ping_delay = timedelta(minutes=1) @@ -198,7 +193,6 @@ class TelegramBareClient: __log__.info('Connecting to %s:%d...', self.session.server_address, self.session.port) - self._main_thread_ident = threading.get_ident() self._background_error = None # Clear previous errors try: @@ -431,6 +425,9 @@ class TelegramBareClient: x.content_related for x in requests): raise TypeError('You can only invoke requests, not types!') + if self._background_error: + raise self._background_error + # For logging purposes if len(requests) == 1: which = type(requests[0]).__name__ @@ -439,66 +436,31 @@ class TelegramBareClient: len(requests), [type(x).__name__ for x in requests]) # 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(): - __log__.debug('Invoking %s from main thread', which) - sender = self._sender - update_state = self.updates - else: - __log__.debug('Invoking %s from background thread. ' - 'Creating temporary connection', which) + __log__.debug('Invoking %s', which) - 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 + call_receive = self._recv_thread is None or self._reconnect_lock.locked() + for retry in range(retries): + result = self._invoke(call_receive, *requests) + if result is not None: + return result - # We should call receive from this thread if there's no background - # thread reading or if the server disconnected us and we're trying - # to reconnect. This is because the read thread may either be - # locked also trying to reconnect or we may be said thread already. - call_receive = not on_main_thread or self._recv_thread is None \ - or self._reconnect_lock.locked() - try: - for attempt in range(retries): - if self._background_error and on_main_thread: - raise self._background_error + __log__.warning('Invoking %s failed %d times, ' + 'reconnecting and retrying', + [str(x) for x in requests], retry + 1) + sleep(1) + # The ReadThread has priority when attempting reconnection, + # since this thread is constantly running while __call__ is + # only done sometimes. Here try connecting only once/retry. + if not self._reconnect_lock.locked(): + with self._reconnect_lock: + self._reconnect() - result = self._invoke( - sender, call_receive, update_state, *requests - ) - if result is not None: - return result - - __log__.warning('Invoking %s failed %d times, ' - 'reconnecting and retrying', - [str(x) for x in requests], attempt + 1) - sleep(1) - # The ReadThread has priority when attempting reconnection, - # since this thread is constantly running while __call__ is - # only done sometimes. Here try connecting only once/retry. - if sender == self._sender: - if not self._reconnect_lock.locked(): - with self._reconnect_lock: - self._reconnect() - else: - sender.connect() - - raise RuntimeError('Number of retries reached 0.') - finally: - if sender != self._sender: - sender.disconnect() # Close temporary connections + raise RuntimeError('Number of retries reached 0.') # Let people use client.invoke(SomeRequest()) instead client(...) invoke = __call__ - def _invoke(self, sender, call_receive, update_state, *requests): + def _invoke(self, call_receive, *requests): try: # Ensure that we start with no previous errors (i.e. resending) for x in requests: @@ -523,7 +485,7 @@ class TelegramBareClient: self._wrap_init_connection(GetConfigRequest()) ) - sender.send(*requests) + self._sender.send(*requests) if not call_receive: # TODO This will be slightly troublesome if we allow @@ -532,11 +494,11 @@ class TelegramBareClient: # in which case a Lock would be required for .receive(). for x in requests: x.confirm_received.wait( - sender.connection.get_timeout() + self._sender.connection.get_timeout() ) else: while not all(x.confirm_received.is_set() for x in requests): - sender.receive(update_state=update_state) + self._sender.receive(update_state=self.updates) except BrokenAuthKeyError: __log__.error('Authorization key seems broken and was invalid!') @@ -578,7 +540,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, update_state, *requests) + return self._invoke(call_receive, *requests) except ServerError as e: # Telegram is having some issues, just retry From 34fe1500962349ef005376f867a2bb045ee43448 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 7 Jan 2018 00:38:30 +0100 Subject: [PATCH 066/361] Save only one auth_key on the database again --- telethon/tl/session.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 930b6973..549bbb29 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -231,6 +231,12 @@ class Session: def _update_session_table(self): with self._db_lock: c = self._conn.cursor() + # While we can save multiple rows into the sessions table + # currently we only want to keep ONE as the tables don't + # tell us which auth_key's are usable and will work. Needs + # some more work before being able to save auth_key's for + # multiple DCs. Probably done differently. + c.execute('delete from sessions') c.execute('insert or replace into sessions values (?,?,?,?)', ( self._dc_id, self._server_address, From 59a1a6aef22c67947489266bdf56609caf8196a7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 7 Jan 2018 16:18:54 +0100 Subject: [PATCH 067/361] Stop working with bytes on the markdown parser --- telethon/extensions/markdown.py | 77 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 6285bf28..10327c46 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -4,6 +4,7 @@ for use within the library, which attempts to handle emojies correctly, since they seem to count as two characters and it's a bit strange. """ import re +import struct from ..tl import TLObject @@ -20,15 +21,24 @@ DEFAULT_DELIMITERS = { '```': MessageEntityPre } -# Regex used to match utf-16le encoded r'\[(.+?)\]\((.+?)\)', -# reason why there's '\0' after every match-literal character. -DEFAULT_URL_RE = re.compile(b'\\[\0(.+?)\\]\0\\(\0(.+?)\\)\0') +# Regex used to match r'\[(.+?)\]\((.+?)\)' (for URLs. +DEFAULT_URL_RE = re.compile(r'\[(.+?)\]\((.+?)\)') # Reverse operation for DEFAULT_URL_RE. {0} for text, {1} for URL. DEFAULT_URL_FORMAT = '[{0}]({1})' -# Encoding to be used -ENC = 'utf-16le' + +def _add_surrogate(text): + return ''.join( + # SMP -> Surrogate Pairs (Telegram offsets are calculated with these). + # See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more. + ''.join(chr(y) for y in struct.unpack(' Date: Mon, 8 Jan 2018 12:01:38 +0100 Subject: [PATCH 068/361] Move utils.calc_msg_key into auth_key (cyclic imports py3.4) --- telethon/crypto/auth_key.py | 5 +++-- telethon/helpers.py | 5 ----- telethon_tests/crypto_test.py | 7 ------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/telethon/crypto/auth_key.py b/telethon/crypto/auth_key.py index 679e62ff..a6c0675b 100644 --- a/telethon/crypto/auth_key.py +++ b/telethon/crypto/auth_key.py @@ -4,7 +4,6 @@ This module holds the AuthKey class. import struct from hashlib import sha1 -from .. import helpers as utils from ..extensions import BinaryReader @@ -36,4 +35,6 @@ class AuthKey: """ new_nonce = new_nonce.to_bytes(32, 'little', signed=True) data = new_nonce + struct.pack(' Date: Mon, 8 Jan 2018 12:14:03 +0100 Subject: [PATCH 069/361] Avoid more cyclic imports on the session file --- telethon/tl/session.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 549bbb29..636d512d 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -2,12 +2,13 @@ import json import os import platform import sqlite3 +import struct import time from base64 import b64decode from os.path import isfile as file_exists from threading import Lock -from .. import utils, helpers +from .. import utils from ..tl import TLObject from ..tl.types import ( PeerUser, PeerChat, PeerChannel, @@ -62,7 +63,7 @@ class Session: self.save_entities = True self.flood_sleep_threshold = 60 - self.id = helpers.generate_random_long(signed=True) + self.id = struct.unpack('q', os.urandom(8))[0] self._sequence = 0 self.time_offset = 0 self._last_msg_id = 0 # Long From 46b088d44c14e856c045e2330a7da5b95881afd6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 8 Jan 2018 12:26:32 +0100 Subject: [PATCH 070/361] Also handle ECONNREFUSED on .connect() (report on #392) --- telethon/extensions/tcp_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index e67c032c..d01c2b13 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -77,7 +77,8 @@ class TcpClient: except OSError as e: # There are some errors that we know how to handle, and # the loop will allow us to retry - if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL): + if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL, + errno.ECONNREFUSED): # Bad file descriptor, i.e. socket was closed, set it # to none to recreate it on the next iteration self._socket = None From 0c3216cb366dfcb88ab09df7ec9f1bbce7a0e0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Vlahovi=C4=87?= Date: Mon, 8 Jan 2018 12:46:47 +0100 Subject: [PATCH 071/361] Fix channel check issue on send_read_acknowledge (#526) --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 6ec8fd02..3bb0f997 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -662,7 +662,7 @@ class TelegramClient(TelegramBareClient): max_id = message.id entity = self.get_input_entity(entity) - if entity == InputPeerChannel: + if isinstance(entity, InputPeerChannel): return self(channels.ReadHistoryRequest(entity, max_id=max_id)) else: return self(messages.ReadHistoryRequest(entity, max_id=max_id)) From c12af5e41296ae84349f6b2c1df3b2feb5ee762f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 8 Jan 2018 14:04:04 +0100 Subject: [PATCH 072/361] Remove references to the wiki --- readthedocs/extra/examples/chats-and-channels.rst | 3 +-- readthedocs/extra/wall-of-shame.rst | 9 ++++----- telethon/telegram_bare_client.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 1bafec80..11e1c624 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -70,7 +70,7 @@ Checking a link without joining If you don't need to join but rather check whether it's a group or a channel, you can use the `CheckChatInviteRequest`__, which takes in -the `hash`__ of said channel or group. +the hash of said channel or group. __ https://lonamiwebs.github.io/Telethon/constructors/chat.html __ https://lonamiwebs.github.io/Telethon/constructors/channel.html @@ -80,7 +80,6 @@ __ https://lonamiwebs.github.io/Telethon/methods/channels/index.html __ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html __ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html __ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html -__ https://github.com/LonamiWebs/Telethon/wiki/Joining-a-chat-or-channel#joining-a-private-chat-or-channel Retrieving all chat members (channels too) diff --git a/readthedocs/extra/wall-of-shame.rst b/readthedocs/extra/wall-of-shame.rst index dfede312..4f7b5660 100644 --- a/readthedocs/extra/wall-of-shame.rst +++ b/readthedocs/extra/wall-of-shame.rst @@ -9,10 +9,9 @@ you to file **issues** whenever you encounter any when working with the library. Said section is **not** for issues on *your* program but rather issues with Telethon itself. -If you have not made the effort to 1. `read through the -wiki `__ and 2. `look for -the method you need `__, you -will end up on the `Wall of +If you have not made the effort to 1. read through the docs and 2. +`look for the method you need `__, +you will end up on the `Wall of Shame `__, i.e. all issues labeled `"RTFM" `__: @@ -31,7 +30,7 @@ i.e. all issues labeled *by Bill M. July 27, 2004* -If you have indeed read the wiki, and have tried looking for the method, +If you have indeed read the docs, and have tried looking for the method, and yet you didn't find what you need, **that's fine**. Telegram's API can have some obscure names at times, and for this reason, there is a `"question" diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 429a4306..498b749e 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -87,7 +87,7 @@ class TelegramBareClient: if not api_id or not api_hash: raise ValueError( "Your API ID or Hash cannot be empty or None. " - "Refer to Telethon's wiki for more information.") + "Refer to telethon.rtfd.io for more information.") self._use_ipv6 = use_ipv6 From 01820c9943cba09637aecb0e1d9928cda1503b45 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 8 Jan 2018 14:18:36 +0100 Subject: [PATCH 073/361] Associate phone code hash with phone (so phone can change) --- telethon/telegram_client.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3bb0f997..5d315ad7 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -142,8 +142,9 @@ class TelegramClient(TelegramBareClient): **kwargs ) - # Some fields to easy signing in - self._phone_code_hash = None + # Some fields to easy signing in. Let {phone: hash} be + # a dictionary because the user may change their mind. + self._phone_code_hash = {} self._phone = None # endregion @@ -167,18 +168,19 @@ class TelegramClient(TelegramBareClient): Information about the result of the request. """ phone = utils.parse_phone(phone) or self._phone + phone_hash = self._phone_code_hash.get(phone) - if not self._phone_code_hash: + if not phone_hash: result = self(SendCodeRequest(phone, self.api_id, self.api_hash)) - self._phone_code_hash = result.phone_code_hash + self._phone_code_hash[phone] = phone_hash = result.phone_code_hash else: force_sms = True self._phone = phone if force_sms: - result = self(ResendCodeRequest(phone, self._phone_code_hash)) - self._phone_code_hash = result.phone_code_hash + result = self(ResendCodeRequest(phone, phone_hash)) + self._phone_code_hash[phone] = result.phone_code_hash return result @@ -218,7 +220,9 @@ class TelegramClient(TelegramBareClient): return self.send_code_request(phone) elif code: phone = utils.parse_phone(phone) or self._phone - phone_code_hash = phone_code_hash or self._phone_code_hash + phone_code_hash = \ + phone_code_hash or self._phone_code_hash.get(phone, None) + if not phone: raise ValueError( 'Please make sure to call send_code_request first.' @@ -274,7 +278,7 @@ class TelegramClient(TelegramBareClient): """ result = self(SignUpRequest( phone_number=self._phone, - phone_code_hash=self._phone_code_hash, + phone_code_hash=self._phone_code_hash.get(self._phone, ''), phone_code=code, first_name=first_name, last_name=last_name From 146a91f83744004e772ddf00c201d6b83d26b341 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 9 Jan 2018 18:04:51 +0100 Subject: [PATCH 074/361] Add a brief description for newcomers --- README.rst | 10 ++++++++++ readthedocs/index.rst | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/README.rst b/README.rst index f524384e..25165b5c 100755 --- a/README.rst +++ b/README.rst @@ -7,6 +7,16 @@ Telethon **Telethon** is Telegram client implementation in **Python 3** which uses the latest available API of Telegram. Remember to use **pip3** to install! + +What is this? +------------- + +Telegram is a popular messaging application. This library is meant +to make it easy for you to write Python programs that can interact +with Telegram. Think of it as a wrapper that has already done the +heavy job for you, so you can focus on developing an application. + + Installing ---------- diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 161c4b1a..cae75541 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -14,6 +14,15 @@ Please follow the links below to get you started, and remember to read the :ref:`changelog` when you upgrade! +What is this? +************* + +Telegram is a popular messaging application. This library is meant +to make it easy for you to write Python programs that can interact +with Telegram. Think of it as a wrapper that has already done the +heavy job for you, so you can focus on developing an application. + + .. _installation-and-usage: .. toctree:: From 045f7f5643539b21f6f46ee1ce2daee76f74a954 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 10 Jan 2018 10:46:43 +0100 Subject: [PATCH 075/361] Assert hash is not None when migrating from JSON sessions --- telethon/tl/session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 636d512d..34427314 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -163,7 +163,8 @@ class Session: rows = [] for p_id, p_hash in data.get('entities', []): - rows.append((p_id, p_hash, None, None, None)) + if p_hash is not None: + rows.append((p_id, p_hash, None, None, None)) return rows except UnicodeDecodeError: return [] # No entities From 8038971753cee1adb4a89ccdda112075286ff54a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 10 Jan 2018 12:50:49 +0100 Subject: [PATCH 076/361] Add clear_mentions parameter to .send_read_acknowledge() --- telethon/telegram_client.py | 40 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5d315ad7..031ff7fb 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -32,7 +32,7 @@ from .tl.functions.contacts import ( from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, - CheckChatInviteRequest + CheckChatInviteRequest, ReadMentionsRequest ) from .tl.functions import channels @@ -639,7 +639,8 @@ class TelegramClient(TelegramBareClient): return messages - def send_read_acknowledge(self, entity, message=None, max_id=None): + def send_read_acknowledge(self, entity, message=None, max_id=None, + clear_mentions=False): """ Sends a "read acknowledge" (i.e., notifying the given peer that we've read their messages, also known as the "double check"). @@ -654,22 +655,37 @@ class TelegramClient(TelegramBareClient): max_id (:obj:`int`): Overrides messages, until which message should the acknowledge should be sent. + + clear_mentions (:obj:`bool`): + Whether the mention badge should be cleared (so that + there are no more mentions) or not for the given entity. + + If no message is provided, this will be the only action + taken. """ if max_id is None: - if not messages: + if message: + if hasattr(message, '__iter__'): + max_id = max(msg.id for msg in message) + else: + max_id = message.id + elif not clear_mentions: raise ValueError( 'Either a message list or a max_id must be provided.') - if hasattr(message, '__iter__'): - max_id = max(msg.id for msg in message) - else: - max_id = message.id - entity = self.get_input_entity(entity) - if isinstance(entity, InputPeerChannel): - return self(channels.ReadHistoryRequest(entity, max_id=max_id)) - else: - return self(messages.ReadHistoryRequest(entity, max_id=max_id)) + if clear_mentions: + self(ReadMentionsRequest(entity)) + if max_id is None: + return True + + if max_id is not None: + if isinstance(entity, InputPeerChannel): + return self(channels.ReadHistoryRequest(entity, max_id=max_id)) + else: + return self(messages.ReadHistoryRequest(entity, max_id=max_id)) + + return False @staticmethod def _get_reply_to(reply_to): From eaef392a9b58aa658bd4e9f46c559f53be181705 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 10 Jan 2018 17:34:34 +0100 Subject: [PATCH 077/361] Add and except missing FLOOD_TEST_PHONE_WAIT_X error --- telethon/telegram_bare_client.py | 7 ++++--- telethon_generator/error_descriptions | 1 + telethon_generator/error_generator.py | 5 ++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 498b749e..14e02acf 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -13,8 +13,9 @@ from . import helpers as utils, version from .crypto import rsa, CdnDecrypter from .errors import ( RPCError, BrokenAuthKeyError, ServerError, - FloodWaitError, FileMigrateError, TypeNotFoundError, - UnauthorizedError, PhoneMigrateError, NetworkMigrateError, UserMigrateError + FloodWaitError, FloodTestPhoneWaitError, FileMigrateError, + TypeNotFoundError, UnauthorizedError, PhoneMigrateError, + NetworkMigrateError, UserMigrateError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode from .tl import TLObject, Session @@ -546,7 +547,7 @@ class TelegramBareClient: # Telegram is having some issues, just retry __log__.error('Telegram servers are having internal errors %s', e) - except FloodWaitError as e: + except (FloodWaitError, FloodTestPhoneWaitError) as e: __log__.warning('Request invoked too often, wait %ds', e.seconds) if e.seconds > self.session.flood_sleep_threshold | 0: raise diff --git a/telethon_generator/error_descriptions b/telethon_generator/error_descriptions index 65894ba1..2754ce5e 100644 --- a/telethon_generator/error_descriptions +++ b/telethon_generator/error_descriptions @@ -63,3 +63,4 @@ SESSION_REVOKED=The authorization has been invalidated, because of the user term 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 +FLOOD_TEST_PHONE_WAIT_X=A wait of {} seconds is required in the test servers diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py index 30163dfc..5b14f22e 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -79,7 +79,9 @@ def generate_code(output, json_file, errors_desc): 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[420].update(( + 'FLOOD_WAIT_X', 'FLOOD_TEST_PHONE_WAIT_X' + )) errors[401].update(( 'AUTH_KEY_INVALID', 'SESSION_EXPIRED', 'SESSION_REVOKED' )) @@ -118,6 +120,7 @@ def generate_code(output, json_file, errors_desc): # Names for the captures, or 'x' if unknown capture_names = { 'FloodWaitError': 'seconds', + 'FloodTestPhoneWaitError': 'seconds', 'FileMigrateError': 'new_dc', 'NetworkMigrateError': 'new_dc', 'PhoneMigrateError': 'new_dc', From 80f81fe69a0709378da99702853c37d8b6706799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Thu, 11 Jan 2018 12:43:47 +0100 Subject: [PATCH 078/361] Added .start() convenience method to quickly connect/authorize (#528) --- README.rst | 6 +- readthedocs/extra/basic/creating-a-client.rst | 19 +++ telethon/telegram_client.py | 125 +++++++++++++++--- 3 files changed, 129 insertions(+), 21 deletions(-) diff --git a/README.rst b/README.rst index 25165b5c..6d9f2c39 100755 --- a/README.rst +++ b/README.rst @@ -39,11 +39,7 @@ Creating a client phone = '+34600000000' client = TelegramClient('session_name', api_id, api_hash) - client.connect() - - # If you already have a previous 'session_name.session' file, skip this. - client.sign_in(phone=phone) - me = client.sign_in(code=77777) # Put whatever code you received here. + client.start() Doing stuff diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 81e19c83..dd468abc 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -76,6 +76,22 @@ As a full example: me = client.sign_in(phone_number, input('Enter code: ')) +All of this, however, can be done through a call to ``.start()``: + + .. code-block:: python + + client = TelegramClient('anon', api_id, api_hash) + client.start() + + +The code shown is just what ``.start()`` will be doing behind the scenes +(with a few extra checks), so that you know how to sign in case you want +to avoid using ``input()`` (the default) for whatever reason. + +You can use either, as both will work. Determining which +is just a matter of taste, and how much control you need. + + .. note:: If you want to use a **proxy**, you have to `install PySocks`__ (via pip or manual) and then set the appropriated parameters: @@ -113,6 +129,9 @@ account, calling :meth:`telethon.TelegramClient.sign_in` will raise a client.sign_in(password=getpass.getpass()) +The mentioned ``.start()`` method will handle this for you as well, but +you must set the ``password=`` parameter beforehand (it won't be asked). + If you don't have 2FA enabled, but you would like to do so through the library, take as example the following code snippet: diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 031ff7fb..9134feef 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,5 +1,6 @@ import itertools import os +import sys import time from collections import OrderedDict, UserList from datetime import datetime, timedelta @@ -14,8 +15,8 @@ from . import TelegramBareClient from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, - PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError -) + PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, + SessionPasswordNeededError) from .network import ConnectionMode from .tl import TLObject from .tl.custom import Draft, Dialog @@ -184,6 +185,104 @@ class TelegramClient(TelegramBareClient): return result + def start(self, phone=None, password=None, bot_token=None, + force_sms=False, code_callback=None): + """ + Convenience method to interactively connect and sign in if required, + also taking into consideration that 2FA may be enabled in the account. + + Example usage: + >>> client = TelegramClient(session, api_id, api_hash).start(phone) + Please enter the code you received: 12345 + Please enter your password: ******* + (You are now logged in) + + Args: + phone (:obj:`str` | :obj:`int`): + The phone to which the code will be sent. + + password (:obj:`callable`, optional): + The password for 2 Factor Authentication (2FA). + This is only required if it is enabled in your account. + + bot_token (:obj:`str`): + Bot Token obtained by @BotFather to log in as a bot. + Cannot be specified with `phone` (only one of either allowed). + + force_sms (:obj:`bool`, optional): + Whether to force sending the code request as SMS. + This only makes sense when signing in with a `phone`. + + code_callback (:obj:`callable`, optional): + A callable that will be used to retrieve the Telegram + login code. Defaults to `input()`. + + Returns: + :obj:`TelegramClient`: + This client, so initialization can be chained with `.start()`. + """ + + if code_callback is None: + def code_callback(): + return input('Please enter the code you received: ') + elif not callable(code_callback): + raise ValueError( + 'The code_callback parameter needs to be a callable ' + 'function that returns the code you received by Telegram.' + ) + + if (phone and bot_token) or (not phone and not bot_token): + raise ValueError( + 'You must provide either a phone number or a bot token, ' + 'not both (or neither).' + ) + + if not self.is_connected(): + self.connect() + + if self.is_user_authorized(): + return self + + if bot_token: + self.sign_in(bot_token=bot_token) + return self + + me = None + attempts = 0 + max_attempts = 3 + two_step_detected = False + + self.send_code_request(phone, force_sms=force_sms) + while attempts < max_attempts: + try: + # Raises SessionPasswordNeededError if 2FA enabled + me = self.sign_in(phone, code_callback()) + break + except SessionPasswordNeededError: + two_step_detected = True + break + except (PhoneCodeEmptyError, PhoneCodeExpiredError, + PhoneCodeHashEmptyError, PhoneCodeInvalidError): + print('Invalid code. Please try again.', file=sys.stderr) + attempts += 1 + else: + raise RuntimeError( + '{} consecutive sign-in attempts failed. Aborting' + .format(max_attempts) + ) + + if two_step_detected: + if not password: + raise ValueError( + "Two-step verification is enabled for this account. " + "Please provide the 'password' argument to 'start()'." + ) + me = self.sign_in(phone=phone, password=password) + + # We won't reach here if any step failed (exit by exception) + print('Signed in successfully as', utils.get_display_name(me)) + return self + def sign_in(self, phone=None, code=None, password=None, bot_token=None, phone_code_hash=None): """ @@ -216,7 +315,7 @@ class TelegramClient(TelegramBareClient): :meth:`.send_code_request()`. """ - if phone and not code: + if phone and not code and not password: return self.send_code_request(phone) elif code: phone = utils.parse_phone(phone) or self._phone @@ -230,15 +329,9 @@ class TelegramClient(TelegramBareClient): if not phone_code_hash: raise ValueError('You also need to provide a phone_code_hash.') - try: - if isinstance(code, int): - code = str(code) - - result = self(SignInRequest(phone, phone_code_hash, code)) - - except (PhoneCodeEmptyError, PhoneCodeExpiredError, - PhoneCodeHashEmptyError, PhoneCodeInvalidError): - return None + # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, + # PhoneCodeHashEmptyError or PhoneCodeInvalidError. + result = self(SignInRequest(phone, phone_code_hash, str(code))) elif password: salt = self(GetPasswordRequest()).current_salt result = self(CheckPasswordRequest( @@ -310,7 +403,7 @@ class TelegramClient(TelegramBareClient): or None if the request fails (hence, not authenticated). Returns: - Your own user. + :obj:`User`: Your own user. """ try: return self(GetUsersRequest([InputUserSelf()]))[0] @@ -779,14 +872,14 @@ class TelegramClient(TelegramBareClient): mime_type = guess_type(file)[0] attr_dict = { DocumentAttributeFilename: - DocumentAttributeFilename(os.path.basename(file)) + DocumentAttributeFilename(os.path.basename(file)) # TODO If the input file is an audio, find out: # Performer and song title and add DocumentAttributeAudio } else: attr_dict = { DocumentAttributeFilename: - DocumentAttributeFilename('unnamed') + DocumentAttributeFilename('unnamed') } if 'is_voice_note' in kwargs: @@ -1305,4 +1398,4 @@ class TelegramClient(TelegramBareClient): 'Make sure you have encountered this peer before.'.format(peer) ) - # endregion + # endregion From 4f441219b164e6f5dee5e12f356869c40c8ba7ef Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 11 Jan 2018 12:45:59 +0100 Subject: [PATCH 079/361] Fix not all docs using new start method --- readthedocs/extra/basic/getting-started.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index 88a6247c..129d752d 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -30,11 +30,7 @@ Creating a client phone = '+34600000000' client = TelegramClient('session_name', api_id, api_hash) - client.connect() - - # If you already have a previous 'session_name.session' file, skip this. - client.sign_in(phone=phone) - me = client.sign_in(code=77777) # Put whatever code you received here. + client.start() **More details**: :ref:`creating-a-client` From 77ef659cbf988047621bd2bc69ea58fdf18f7393 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 11 Jan 2018 15:41:57 +0100 Subject: [PATCH 080/361] Clearer error when invoking without calling .connect() (#532) --- 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 14e02acf..8adf2567 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -508,14 +508,14 @@ class TelegramBareClient: except TimeoutError: __log__.warning('Invoking timed out') # We will just retry - except ConnectionResetError: + except ConnectionResetError as e: __log__.warning('Connection was reset while invoking') if self._user_connected: # Server disconnected us, __call__ will try reconnecting. return None else: # User never called .connect(), so raise this error. - raise + raise RuntimeError('Tried to invoke without .connect()') from e # Clear the flag if we got this far self._first_request = False From 1fd20ace2c820cb20aa52c546c8eec8979e97d66 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 11 Jan 2018 22:18:58 +0100 Subject: [PATCH 081/361] Update to v0.16.1 --- readthedocs/extra/changelog.rst | 41 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 569f21ca..9457c4f4 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,47 @@ it can take advantage of new goodies! .. contents:: List of All Versions +MtProto 2.0 (v0.16.1) +===================== + +*Published at 2018/01/11* + ++-----------------------+ +| Scheme layer used: 74 | ++-----------------------+ + +The library is now using MtProto 2.0! This shouldn't really affect you +as an end user, but at least it means the library will be ready by the +time MtProto 1.0 is deprecated. + +Additions +~~~~~~~~~ + +- New ``.start()`` method, to make the library avoid boilerplate code. +- ``.send_file`` accepts a new optional ``thumbnail`` parameter, and + returns the ``Message`` with the sent file. + + +Bug fixes +~~~~~~~~~ + +- The library uses again only a single connection. Less updates are + be dropped now, and the performance is even better than using temporary + connections. +- ``without rowid`` will only be used on the ``*.session`` if supported. +- Phone code hash is associated with phone, so you can change your mind + when calling ``.sign_in()``. + + +Internal changes +~~~~~~~~~~~~~~~~ + +- File cache now relies on the hash of the file uploaded instead its path, + and is now persistent in the ``*.session`` file. Report any bugs on this! +- Clearer error when invoking without being connected. +- Markdown parser doesn't work on bytes anymore (which makes it cleaner). + + Sessions as sqlite databases (v0.16) ==================================== diff --git a/telethon/version.py b/telethon/version.py index e7fcc442..e0220c01 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.16' +__version__ = '0.16.1' From 6cb8f2e3da3e330c8461eb9672f296a1e1cf71ec Mon Sep 17 00:00:00 2001 From: Noah Overcash Date: Fri, 12 Jan 2018 04:08:40 -0500 Subject: [PATCH 082/361] Update pip references with pip3 (#527) --- README.rst | 2 +- readthedocs/extra/basic/getting-started.rst | 2 +- readthedocs/extra/basic/installation.rst | 13 ++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 6d9f2c39..09ddaf90 100755 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Installing .. code:: sh - pip install telethon + pip3 install telethon Creating a client diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index 129d752d..912ea768 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -11,7 +11,7 @@ Getting Started Simple Installation ******************* - ``pip install telethon`` + ``pip3 install telethon`` **More details**: :ref:`installation` diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index b4fb1ac2..945576d0 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -10,21 +10,20 @@ Automatic Installation To install Telethon, simply do: - ``pip install telethon`` + ``pip3 install telethon`` -If you get something like ``"SyntaxError: invalid syntax"`` or any other -error while installing/importing the library, it's probably because ``pip`` -defaults to Python 2, which is not supported. Use ``pip3`` instead. +Needless to say, you must have Python 3 and PyPi installed in your system. +See https://python.org and https://pypi.python.org/pypi/pip for more. If you already have the library installed, upgrade with: - ``pip install --upgrade telethon`` + ``pip3 install --upgrade telethon`` You can also install the library directly from GitHub or a fork: .. code-block:: sh - # pip install git+https://github.com/LonamiWebs/Telethon.git + # pip3 install git+https://github.com/LonamiWebs/Telethon.git or $ git clone https://github.com/LonamiWebs/Telethon.git $ cd Telethon/ @@ -39,7 +38,7 @@ Manual Installation 1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and ``rsa`` (`GitHub`__ | `PyPi`__) modules: - ``sudo -H pip install pyaes rsa`` + ``sudo -H pip3 install pyaes rsa`` 2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git`` From ef3ea11e38dd6c302543db636c0703d5dd10027e Mon Sep 17 00:00:00 2001 From: Lonami Date: Fri, 12 Jan 2018 18:21:02 +0100 Subject: [PATCH 083/361] Remove pesky minus character hiding example --- readthedocs/extra/examples/chats-and-channels.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 11e1c624..99ce235f 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -41,7 +41,7 @@ enough information to join! The part after the example, is the ``hash`` of the chat or channel. Now you can use `ImportChatInviteRequest`__ as follows: - .. -block:: python + .. code-block:: python from telethon.tl.functions.messages import ImportChatInviteRequest updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) From 77301378f85ba4976c02e7e5704cdde88cd501a0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 13 Jan 2018 11:54:41 +0100 Subject: [PATCH 084/361] Make .start() more friendly by asking phone if not given Ping #530 --- readthedocs/extra/basic/creating-a-client.rst | 6 ++++-- readthedocs/extra/basic/getting-started.rst | 1 - telethon/telegram_client.py | 10 +++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index dd468abc..10ae5f60 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -31,7 +31,6 @@ one is very simple: # Use your own values here api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - phone_number = '+34600000000' client = TelegramClient('some_name', api_id, api_hash) @@ -54,6 +53,7 @@ If you're not authorized, you need to ``.sign_in()``: .. code-block:: python + phone_number = '+34600000000' client.send_code_request(phone_number) myself = client.sign_in(phone_number, input('Enter code: ')) # If .sign_in raises PhoneNumberUnoccupiedError, use .sign_up instead @@ -86,7 +86,9 @@ All of this, however, can be done through a call to ``.start()``: The code shown is just what ``.start()`` will be doing behind the scenes (with a few extra checks), so that you know how to sign in case you want -to avoid using ``input()`` (the default) for whatever reason. +to avoid using ``input()`` (the default) for whatever reason. If no phone +or bot token is provided, you will be asked one through ``input()``. The +method also accepts a ``phone=`` and ``bot_token`` parameters. You can use either, as both will work. Determining which is just a matter of taste, and how much control you need. diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index 912ea768..e69cc3ef 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -27,7 +27,6 @@ Creating a client # api_hash from https://my.telegram.org, under API Development. api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - phone = '+34600000000' client = TelegramClient('session_name', api_id, api_hash) client.start() diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9134feef..98b22940 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -231,7 +231,15 @@ class TelegramClient(TelegramBareClient): 'function that returns the code you received by Telegram.' ) - if (phone and bot_token) or (not phone and not bot_token): + if not phone and not bot_token: + value = input('Please enter your phone/bot token: ') + phone = utils.parse_phone(phone) + if not phone: + bot_token = value + print("Note: input doesn't look like a phone, " + "using as bot token") + + if phone and bot_token: raise ValueError( 'You must provide either a phone number or a bot token, ' 'not both (or neither).' From 0d429f55c54ce8b89a9df6222229a51be5d3a6bf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 13 Jan 2018 12:00:53 +0100 Subject: [PATCH 085/361] Fix asking for phone on .start() --- README.rst | 1 - telethon/telegram_client.py | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 09ddaf90..6343e6e1 100755 --- a/README.rst +++ b/README.rst @@ -36,7 +36,6 @@ Creating a client # api_hash from https://my.telegram.org, under API Development. api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - phone = '+34600000000' client = TelegramClient('session_name', api_id, api_hash) client.start() diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 98b22940..674e6045 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -232,14 +232,10 @@ class TelegramClient(TelegramBareClient): ) if not phone and not bot_token: - value = input('Please enter your phone/bot token: ') - phone = utils.parse_phone(phone) - if not phone: - bot_token = value - print("Note: input doesn't look like a phone, " - "using as bot token") + while not phone: + phone = utils.parse_phone(input('Please enter your phone: ')) - if phone and bot_token: + elif phone and bot_token: raise ValueError( 'You must provide either a phone number or a bot token, ' 'not both (or neither).' From c5e969d5854bbb44d895fcf19563606aaecab07e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 13 Jan 2018 19:26:45 +0100 Subject: [PATCH 086/361] Add more useful logging on invalid packet length received --- telethon/network/connection.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/telethon/network/connection.py b/telethon/network/connection.py index ff255d00..0adaf98a 100644 --- a/telethon/network/connection.py +++ b/telethon/network/connection.py @@ -2,6 +2,7 @@ This module holds both the Connection class and the ConnectionMode enum, which specifies the protocol to be used by the Connection. """ +import logging import os import struct from datetime import timedelta @@ -14,6 +15,8 @@ from ..crypto import AESModeCTR from ..extensions import TcpClient from ..errors import InvalidChecksumError +__log__ = logging.getLogger(__name__) + class ConnectionMode(Enum): """Represents which mode should be used to stabilise a connection. @@ -181,6 +184,21 @@ class Connection: packet_len_seq = self.read(8) # 4 and 4 packet_len, seq = struct.unpack(' Date: Sun, 14 Jan 2018 10:53:29 +0100 Subject: [PATCH 087/361] Note the errors package on the RPC errors section --- readthedocs/extra/troubleshooting/rpc-errors.rst | 7 ++++--- readthedocs/telethon.errors.rst | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/readthedocs/extra/troubleshooting/rpc-errors.rst b/readthedocs/extra/troubleshooting/rpc-errors.rst index 0d36bec6..17299f1f 100644 --- a/readthedocs/extra/troubleshooting/rpc-errors.rst +++ b/readthedocs/extra/troubleshooting/rpc-errors.rst @@ -2,10 +2,11 @@ RPC Errors ========== -RPC stands for Remote Procedure Call, and when Telethon raises an -``RPCError``, it's most likely because you have invoked some of the API +RPC stands for Remote Procedure Call, and when the library raises +a ``RPCError``, it's because you have invoked some of the API methods incorrectly (wrong parameters, wrong permissions, or even -something went wrong on Telegram's server). The most common are: +something went wrong on Telegram's server). All the errors are +available in :ref:`telethon-errors-package`, but some examples are: - ``FloodWaitError`` (420), the same request was repeated many times. Must wait ``.seconds`` (you can access this parameter). diff --git a/readthedocs/telethon.errors.rst b/readthedocs/telethon.errors.rst index 2e94fe33..e90d1819 100644 --- a/readthedocs/telethon.errors.rst +++ b/readthedocs/telethon.errors.rst @@ -1,3 +1,6 @@ +.. _telethon-errors-package: + + telethon\.errors package ======================== From 8be7e76b741db51f7160d0e8868b089aa029e77f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 14 Jan 2018 21:20:22 +0100 Subject: [PATCH 088/361] Use the idling state instead checking if read thread is present This caused some multithreading bugs, for instance, when there was no read thread and the main thread was idling, and there were some update workers. Several threads would try to read from the socket at the same time (since there's no lock for reading), causing reads to be corrupted and receiving "invalid packet lengths" from the network. Closes #538. --- telethon/telegram_bare_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 8adf2567..2c1b6188 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -163,6 +163,7 @@ class TelegramBareClient: # if the user has left enabled such option. self._spawn_read_thread = spawn_read_thread self._recv_thread = None + self._idling = threading.Event() # Default PingRequest delay self._last_ping = datetime.now() @@ -438,8 +439,9 @@ class TelegramBareClient: # Determine the sender to be used (main or a new connection) __log__.debug('Invoking %s', which) + call_receive = \ + not self._idling.is_set() or self._reconnect_lock.locked() - call_receive = self._recv_thread is None or self._reconnect_lock.locked() for retry in range(retries): result = self._invoke(call_receive, *requests) if result is not None: @@ -829,6 +831,7 @@ class TelegramBareClient: if self._spawn_read_thread and not self._on_read_thread(): raise RuntimeError('Can only idle if spawn_read_thread=False') + self._idling.set() for sig in stop_signals: signal(sig, self._signal_handler) @@ -857,7 +860,11 @@ class TelegramBareClient: with self._reconnect_lock: while self._user_connected and not self._reconnect(): sleep(0.1) # Retry forever, this is instant messaging + except: + self._idling.clear() + raise + self._idling.clear() __log__.info('Connection closed by the user, not reading anymore') # By using this approach, another thread will be From 00859d52c354f7b5ac27293a2e3b1f229004aef2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 15 Jan 2018 09:48:37 +0100 Subject: [PATCH 089/361] Ask for the phone on start only if required --- telethon/telegram_client.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 674e6045..1fccda50 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -185,7 +185,9 @@ class TelegramClient(TelegramBareClient): return result - def start(self, phone=None, password=None, bot_token=None, + def start(self, + phone=lambda: input('Please enter your phone: '), + password=None, bot_token=None, force_sms=False, code_callback=None): """ Convenience method to interactively connect and sign in if required, @@ -198,8 +200,9 @@ class TelegramClient(TelegramBareClient): (You are now logged in) Args: - phone (:obj:`str` | :obj:`int`): - The phone to which the code will be sent. + phone (:obj:`str` | :obj:`int` | :obj:`callable`): + The phone (or callable without arguments to get it) + to which the code will be sent. password (:obj:`callable`, optional): The password for 2 Factor Authentication (2FA). @@ -232,14 +235,11 @@ class TelegramClient(TelegramBareClient): ) if not phone and not bot_token: - while not phone: - phone = utils.parse_phone(input('Please enter your phone: ')) + raise ValueError('No phone number or bot token provided.') - elif phone and bot_token: - raise ValueError( - 'You must provide either a phone number or a bot token, ' - 'not both (or neither).' - ) + if phone and bot_token: + raise ValueError('Both a phone and a bot token provided, ' + 'must only provide one of either') if not self.is_connected(): self.connect() @@ -251,6 +251,10 @@ class TelegramClient(TelegramBareClient): self.sign_in(bot_token=bot_token) return self + # Turn the callable into a valid phone number + while callable(phone): + phone = utils.parse_phone(phone()) or phone + me = None attempts = 0 max_attempts = 3 From 494c90af692648f9b6f8cfc69c1860a51c46b41c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 15 Jan 2018 12:36:46 +0100 Subject: [PATCH 090/361] Fix uploaded files cache may have expired --- telethon/telegram_bare_client.py | 5 ++++- telethon/telegram_client.py | 27 +++++++++++++++++++++------ telethon/tl/session.py | 8 ++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 2c1b6188..d2e84ee6 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -571,6 +571,7 @@ class TelegramBareClient: file, part_size_kb=None, file_name=None, + allow_cache=True, progress_callback=None): """Uploads the specified file and returns a handle (an instance of InputFile or InputFileBig, as required) which can be later used. @@ -633,10 +634,12 @@ class TelegramBareClient: file = stream.read() hash_md5 = md5(file) tuple_ = self.session.get_file(hash_md5.digest(), file_size) - if tuple_: + if tuple_ and allow_cache: __log__.info('File was already cached, not uploading again') return InputFile(name=file_name, md5_checksum=tuple_[0], id=tuple_[2], parts=tuple_[3]) + elif tuple_ and not allow_cache: + self.session.clear_file(hash_md5.digest(), file_size) else: hash_md5 = None diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 1fccda50..ac06b8eb 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -16,7 +16,7 @@ from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, - SessionPasswordNeededError) + SessionPasswordNeededError, FilePartMissingError) from .network import ConnectionMode from .tl import TLObject from .tl.custom import Draft, Dialog @@ -813,6 +813,7 @@ class TelegramClient(TelegramBareClient): reply_to=None, attributes=None, thumb=None, + allow_cache=True, **kwargs): """ Sends a file to the specified entity. @@ -849,9 +850,13 @@ class TelegramClient(TelegramBareClient): Optional attributes that override the inferred ones, like ``DocumentAttributeFilename`` and so on. - thumb (:obj:`str` | :obj:`bytes` | :obj:`file`): + thumb (:obj:`str` | :obj:`bytes` | :obj:`file`, optional): Optional thumbnail (for videos). + allow_cache (:obj:`bool`, optional): + Whether to allow using the cached version stored in the + database or not. Defaults to ``True`` to avoid reuploads. + Kwargs: 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. @@ -868,7 +873,7 @@ class TelegramClient(TelegramBareClient): ) file_handle = self.upload_file( - file, progress_callback=progress_callback) + file, progress_callback=progress_callback, allow_cache=allow_cache) if as_photo and not force_document: media = InputMediaUploadedPhoto(file_handle, caption) @@ -926,9 +931,19 @@ class TelegramClient(TelegramBareClient): media=media, reply_to_msg_id=self._get_reply_to(reply_to) ) - result = self(request) - - return self._get_response_message(request, result) + try: + return self._get_response_message(request, self(request)) + except FilePartMissingError: + # After a while, cached files are invalidated and this + # error is raised. The file needs to be uploaded again. + if not allow_cache: + raise + return self.send_file( + entity, file, allow_cache=False, + caption=caption, force_document=force_document, + progress_callback=progress_callback, reply_to=reply_to, + attributes=attributes, thumb=thumb, **kwargs + ) def send_voice_note(self, entity, file, caption='', upload_progress=None, reply_to=None): diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 34427314..1dbf99c5 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -433,3 +433,11 @@ class Session: (md5_digest, file_size, file_id, part_count) ) self.save() + + def clear_file(self, md5_digest, file_size): + with self._db_lock: + self._conn.execute( + 'delete from sent_files where ' + 'md5_digest = ? and file_size = ?', (md5_digest, file_size) + ) + self.save() From 36e210191085fbc822d394340de2f5033a4d2c2b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 15 Jan 2018 18:15:30 +0100 Subject: [PATCH 091/361] Allow sending multiple files as album (closes #455) --- telethon/telegram_client.py | 103 ++++++++++++++++++++++++++++-------- telethon/utils.py | 6 +++ 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ac06b8eb..f5c10e66 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -33,7 +33,8 @@ from .tl.functions.contacts import ( from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, - CheckChatInviteRequest, ReadMentionsRequest + CheckChatInviteRequest, ReadMentionsRequest, + SendMultiMediaRequest, UploadMediaRequest ) from .tl.functions import channels @@ -53,7 +54,8 @@ from .tl.types import ( InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, - ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf + ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, + InputSingleMedia, InputMediaPhoto, InputPhoto ) from .tl.types.messages import DialogsSlice from .extensions import markdown @@ -512,15 +514,21 @@ class TelegramClient(TelegramBareClient): @staticmethod def _get_response_message(request, result): - """Extracts the response message known a request and Update result""" + """ + Extracts the response message known a request and Update result. + The request may also be the ID of the message to match. + """ # Telegram seems to send updateMessageID first, then updateNewMessage, # however let's not rely on that just in case. - msg_id = None - for update in result.updates: - if isinstance(update, UpdateMessageID): - if update.random_id == request.random_id: - msg_id = update.id - break + if isinstance(request, int): + msg_id = request + else: + msg_id = None + for update in result.updates: + if isinstance(update, UpdateMessageID): + if update.random_id == request.random_id: + msg_id = update.id + break for update in result.updates: if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)): @@ -861,21 +869,34 @@ class TelegramClient(TelegramBareClient): 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. - Returns: - The message containing the sent file. + Returns: + The message (or messages) containing the sent file. """ - as_photo = False - if isinstance(file, str): - lowercase_file = file.lower() - as_photo = any( - lowercase_file.endswith(ext) - for ext in ('.png', '.jpg', '.gif', '.jpeg') - ) + # First check if the user passed an iterable, in which case + # we may want to send as an album if all are photo files. + if hasattr(file, '__iter__'): + # Convert to tuple so we can iterate several times + file = tuple(x for x in file) + if all(utils.is_image(x) for x in file): + return self._send_album( + entity, file, caption=caption, + progress_callback=progress_callback, reply_to=reply_to, + allow_cache=allow_cache + ) + # Not all are images, so send all the files one by one + return [ + self.send_file( + entity, x, allow_cache=False, + caption=caption, force_document=force_document, + progress_callback=progress_callback, reply_to=reply_to, + attributes=attributes, thumb=thumb, **kwargs + ) for x in file + ] file_handle = self.upload_file( file, progress_callback=progress_callback, allow_cache=allow_cache) - if as_photo and not force_document: + if utils.is_image(file) and not force_document: media = InputMediaUploadedPhoto(file_handle, caption) else: mime_type = None @@ -945,14 +966,52 @@ class TelegramClient(TelegramBareClient): attributes=attributes, thumb=thumb, **kwargs ) - def send_voice_note(self, entity, file, caption='', upload_progress=None, + def send_voice_note(self, entity, file, caption='', progress_callback=None, reply_to=None): """Wrapper method around .send_file() with is_voice_note=()""" return self.send_file(entity, file, caption, - upload_progress=upload_progress, + progress_callback=progress_callback, reply_to=reply_to, is_voice_note=()) # empty tuple is enough + def _send_album(self, entity, files, caption='', + progress_callback=None, reply_to=None, + allow_cache=True): + """Specialized version of .send_file for albums""" + entity = self.get_input_entity(entity) + reply_to = self._get_reply_to(reply_to) + try: + # Need to upload the media first + media = [ + self(UploadMediaRequest(entity, InputMediaUploadedPhoto( + self.upload_file(file), + caption=caption + ))) + for file in files + ] + # Now we can construct the multi-media request + result = self(SendMultiMediaRequest( + entity, reply_to_msg_id=reply_to, multi_media=[ + InputSingleMedia(InputMediaPhoto( + InputPhoto(m.photo.id, m.photo.access_hash), + caption=caption + )) + for m in media + ] + )) + return [ + self._get_response_message(update.id, result) + for update in result.updates + if isinstance(update, UpdateMessageID) + ] + except FilePartMissingError: + if not allow_cache: + raise + return self._send_album( + entity, files, allow_cache=False, caption=caption, + progress_callback=progress_callback, reply_to=reply_to + ) + # endregion # region Downloading media requests @@ -1421,4 +1480,4 @@ class TelegramClient(TelegramBareClient): 'Make sure you have encountered this peer before.'.format(peer) ) - # endregion + # endregion diff --git a/telethon/utils.py b/telethon/utils.py index 48c867d1..b1053504 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -312,6 +312,12 @@ def get_input_media(media, user_caption=None, is_photo=False): _raise_cast_fail(media, 'InputMedia') +def is_image(file): + """Returns True if the file extension looks like an image file""" + return (isinstance(file, str) and + bool(re.search(r'\.(png|jpe?g|gif)$', file, re.IGNORECASE))) + + def parse_phone(phone): """Parses the given phone, or returns None if it's invalid""" if isinstance(phone, int): From 2ccb6063e0f3784035d1b2e064f2da212a6e5346 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 15 Jan 2018 18:46:04 +0100 Subject: [PATCH 092/361] Call gen_tl() when installing through setup.py (#530) --- setup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0c531d70..2682e099 100755 --- a/setup.py +++ b/setup.py @@ -45,11 +45,13 @@ GENERATOR_DIR = 'telethon/tl' IMPORT_DEPTH = 2 -def gen_tl(): +def gen_tl(force=True): from telethon_generator.tl_generator import TLGenerator from telethon_generator.error_generator import generate_code generator = TLGenerator(GENERATOR_DIR) if generator.tlobjects_exist(): + if not force: + return print('Detected previous TLObjects. Cleaning...') generator.clean_tlobjects() @@ -99,6 +101,10 @@ def main(): fetch_errors(ERRORS_JSON) else: + # Call gen_tl() if the scheme.tl file exists, e.g. install from GitHub + if os.path.isfile(SCHEME_TL): + gen_tl(force=False) + # Get the long description from the README file with open('README.rst', encoding='utf-8') as f: long_description = f.read() From 49f204c95546e7f368d5854680e9cd30adf1d7c0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 16 Jan 2018 14:01:14 +0100 Subject: [PATCH 093/361] Fix .get_input_media using None caption and missing venue type --- telethon/utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index b1053504..8549e18d 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -248,15 +248,17 @@ def get_input_media(media, user_caption=None, is_photo=False): if isinstance(media, MessageMediaPhoto): return InputMediaPhoto( id=get_input_photo(media.photo), - caption=media.caption if user_caption is None else user_caption, - ttl_seconds=media.ttl_seconds + ttl_seconds=media.ttl_seconds, + caption=((media.caption if user_caption is None else user_caption) + or '') ) if isinstance(media, MessageMediaDocument): return InputMediaDocument( id=get_input_document(media.document), - caption=media.caption if user_caption is None else user_caption, - ttl_seconds=media.ttl_seconds + ttl_seconds=media.ttl_seconds, + caption=((media.caption if user_caption is None else user_caption) + or '') ) if isinstance(media, FileLocation): @@ -298,7 +300,8 @@ def get_input_media(media, user_caption=None, is_photo=False): title=media.title, address=media.address, provider=media.provider, - venue_id=media.venue_id + venue_id=media.venue_id, + venue_type='' ) if isinstance(media, ( From fde0d60f726df8f7887c9fb83f3b532effa2164d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 16 Jan 2018 18:36:50 +0100 Subject: [PATCH 094/361] Update old interactive example (#546) --- telethon/telegram_client.py | 1 - .../interactive_telegram_client.py | 65 ++++++++----------- 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f5c10e66..9e192028 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -728,7 +728,6 @@ class TelegramClient(TelegramBareClient): # Add a few extra attributes to the Message to make it friendlier. messages.total = total_messages for m in messages: - # TODO Better way to return a total without tuples? m.sender = (None if not m.from_id else entities[utils.get_peer_id(m.from_id)]) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 501d557b..d45d2ff1 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -84,9 +84,9 @@ class InteractiveTelegramClient(TelegramClient): update_workers=1 ) - # Store all the found media in memory here, - # so it can be downloaded if the user wants - self.found_media = set() + # Store {message.id: message} map here so that we can download + # media known the message ID, for every message having media. + self.found_media = {} # Calling .connect() may return False, so you need to assert it's # True before continuing. Otherwise you may want to retry as done here. @@ -204,27 +204,21 @@ class InteractiveTelegramClient(TelegramClient): # History elif msg == '!h': # First retrieve the messages and some information - total_count, messages, senders = \ - self.get_message_history(entity, limit=10) + messages = self.get_message_history(entity, limit=10) # Iterate over all (in reverse order so the latest appear # the last in the console) and print them with format: # "[hh:mm] Sender: Message" - for msg, sender in zip( - reversed(messages), reversed(senders)): - # Get the name of the sender if any - if sender: - name = getattr(sender, 'first_name', None) - if not name: - name = getattr(sender, 'title') - if not name: - name = '???' - else: - name = '???' + for msg in reversed(messages): + # Note that the .sender attribute is only there for + # convenience, the API returns it differently. But + # this shouldn't concern us. See the documentation + # for .get_message_history() for more information. + name = get_display_name(msg.sender) # Format the message content if getattr(msg, 'media', None): - self.found_media.add(msg) + self.found_media[msg.id] = msg # The media may or may not have a caption caption = getattr(msg.media, 'caption', '') content = '<{}> {}'.format( @@ -257,8 +251,7 @@ class InteractiveTelegramClient(TelegramClient): elif msg.startswith('!d '): # Slice the message to get message ID deleted_msg = self.delete_messages(entity, msg[len('!d '):]) - print('Deleted. {}'.format(deleted_msg)) - + print('Deleted {}'.format(deleted_msg)) # Download media elif msg.startswith('!dm '): @@ -275,12 +268,11 @@ class InteractiveTelegramClient(TelegramClient): 'Profile picture downloaded to {}'.format(output) ) else: - print('No profile picture found for this user.') + print('No profile picture found for this user!') # Send chat message (if any) elif msg: - self.send_message( - entity, msg, link_preview=False) + self.send_message(entity, msg, link_preview=False) def send_photo(self, path, entity): """Sends the file located at path to the desired entity as a photo""" @@ -304,23 +296,20 @@ class InteractiveTelegramClient(TelegramClient): downloads it. """ try: - # The user may have entered a non-integer string! - msg_media_id = int(media_id) + msg = self.found_media[int(media_id)] + except (ValueError, KeyError): + # ValueError when parsing, KeyError when accessing dictionary + print('Invalid media ID given or message not found!') + return - # Search the message ID - for msg in self.found_media: - if msg.id == msg_media_id: - print('Downloading media to usermedia/...') - os.makedirs('usermedia', exist_ok=True) - output = self.download_media( - msg.media, - file='usermedia/', - progress_callback=self.download_progress_callback - ) - print('Media downloaded to {}!'.format(output)) - - except ValueError: - print('Invalid media ID given!') + print('Downloading media to usermedia/...') + os.makedirs('usermedia', exist_ok=True) + output = self.download_media( + msg.media, + file='usermedia/', + progress_callback=self.download_progress_callback + ) + print('Media downloaded to {}!'.format(output)) @staticmethod def download_progress_callback(downloaded_bytes, total_bytes): From bfe9378054c30e59b37afaf5c2202f56c3f8ea3d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 17 Jan 2018 13:28:56 +0100 Subject: [PATCH 095/361] Fix .send_file failing with strings (as they are iterable) --- 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 9e192028..fdd96d47 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -873,7 +873,7 @@ class TelegramClient(TelegramBareClient): """ # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. - if hasattr(file, '__iter__'): + if hasattr(file, '__iter__') and not isinstance(file, (str, bytes)): # Convert to tuple so we can iterate several times file = tuple(x for x in file) if all(utils.is_image(x) for x in file): @@ -1321,7 +1321,7 @@ class TelegramClient(TelegramBareClient): ``User``, ``Chat`` or ``Channel`` corresponding to the input entity. """ - if not isinstance(entity, str) and hasattr(entity, '__iter__'): + if hasattr(entity, '__iter__') and not isinstance(entity, str): single = False else: single = True From 428abebed8f429397c672a9d428d18b5a8051d38 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 17 Jan 2018 13:29:08 +0100 Subject: [PATCH 096/361] Fix sending albums failing on invalid cache --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index fdd96d47..84c9beea 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -983,7 +983,7 @@ class TelegramClient(TelegramBareClient): # Need to upload the media first media = [ self(UploadMediaRequest(entity, InputMediaUploadedPhoto( - self.upload_file(file), + self.upload_file(file, allow_cache=allow_cache), caption=caption ))) for file in files From 55efb2b104f8e1d827a7e947983938df173a77b9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 09:52:39 +0100 Subject: [PATCH 097/361] Use a different schema for file cache which actually persists Caching the inputFile values would not persist accross several days so the cache was nearly unnecessary. Saving the id/hash of the actual inputMedia sent is a much better/persistent idea. --- telethon/telegram_bare_client.py | 10 ---- telethon/tl/session.py | 84 ++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index d2e84ee6..af86c0f1 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -633,13 +633,6 @@ class TelegramBareClient: with open(file, 'rb') as stream: file = stream.read() hash_md5 = md5(file) - tuple_ = self.session.get_file(hash_md5.digest(), file_size) - if tuple_ and allow_cache: - __log__.info('File was already cached, not uploading again') - return InputFile(name=file_name, - md5_checksum=tuple_[0], id=tuple_[2], parts=tuple_[3]) - elif tuple_ and not allow_cache: - self.session.clear_file(hash_md5.digest(), file_size) else: hash_md5 = None @@ -673,9 +666,6 @@ class TelegramBareClient: if is_large: return InputFileBig(file_id, part_count, file_name) else: - self.session.cache_file( - hash_md5.digest(), file_size, file_id, part_count) - return InputFile(file_id, part_count, file_name, md5_checksum=hash_md5.hexdigest()) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 1dbf99c5..5d89a5f7 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -5,6 +5,7 @@ import sqlite3 import struct import time from base64 import b64decode +from enum import Enum from os.path import isfile as file_exists from threading import Lock @@ -12,11 +13,26 @@ from .. import utils from ..tl import TLObject from ..tl.types import ( PeerUser, PeerChat, PeerChannel, - InputPeerUser, InputPeerChat, InputPeerChannel + InputPeerUser, InputPeerChat, InputPeerChannel, + InputPhoto, InputDocument ) EXTENSION = '.session' -CURRENT_VERSION = 2 # database version +CURRENT_VERSION = 3 # database version + + +class _SentFileType(Enum): + DOCUMENT = 0 + PHOTO = 1 + + @staticmethod + def from_type(cls): + if cls == InputDocument: + return _SentFileType.DOCUMENT + elif cls == InputPhoto: + return _SentFileType.PHOTO + else: + raise ValueError('The cls must be either InputDocument/InputPhoto') class Session: @@ -130,9 +146,10 @@ class Session: """sent_files ( md5_digest blob, file_size integer, - file_id integer, - part_count integer, - primary key(md5_digest, file_size) + type integer, + id integer, + hash integer, + primary key(md5_digest, file_size, type) )""" ) c.execute("insert into version values (?)", (CURRENT_VERSION,)) @@ -171,18 +188,22 @@ class Session: def _upgrade_database(self, old): c = self._conn.cursor() - if old == 1: - self._create_table(c,"""sent_files ( - md5_digest blob, - file_size integer, - file_id integer, - part_count integer, - primary key(md5_digest, file_size) - )""") - old = 2 + # old == 1 doesn't have the old sent_files so no need to drop + if old == 2: + # Old cache from old sent_files lasts then a day anyway, drop + c.execute('drop table sent_files') + self._create_table(c, """sent_files ( + md5_digest blob, + file_size integer, + type integer, + id integer, + hash integer, + primary key(md5_digest, file_size, type) + )""") c.close() - def _create_table(self, c, *definitions): + @staticmethod + def _create_table(c, *definitions): """ Creates a table given its definition 'name (columns). If the sqlite version is >= 3.8.2, it will use "without rowid". @@ -420,24 +441,25 @@ class Session: # File processing - def get_file(self, md5_digest, file_size): - return self._conn.execute( - 'select * from sent_files ' - 'where md5_digest = ? and file_size = ?', (md5_digest, file_size) + def get_file(self, md5_digest, file_size, cls): + tuple_ = self._conn.execute( + 'select id, hash from sent_files ' + 'where md5_digest = ? and file_size = ? and type = ?', + (md5_digest, file_size, _SentFileType.from_type(cls)) ).fetchone() + if tuple_: + # Both allowed classes have (id, access_hash) as parameters + return cls(tuple_[0], tuple_[1]) + + def cache_file(self, md5_digest, file_size, instance): + if not isinstance(instance, (InputDocument, InputPhoto)): + raise TypeError('Cannot cache %s instance' % type(instance)) - def cache_file(self, md5_digest, file_size, file_id, part_count): with self._db_lock: self._conn.execute( - 'insert into sent_files values (?,?,?,?)', - (md5_digest, file_size, file_id, part_count) - ) - self.save() - - def clear_file(self, md5_digest, file_size): - with self._db_lock: - self._conn.execute( - 'delete from sent_files where ' - 'md5_digest = ? and file_size = ?', (md5_digest, file_size) - ) + 'insert into sent_files values (?,?,?,?,?)', ( + md5_digest, file_size, + _SentFileType.from_type(type(instance)), + instance.id, instance.access_hash + )) self.save() From 1a3feec481f33035f356971ef1f8a7f7cc9d0d48 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 13:55:03 +0100 Subject: [PATCH 098/361] Move upload/download file methods to the TelegramClient --- telethon/telegram_bare_client.py | 217 +-------------------------- telethon/telegram_client.py | 248 ++++++++++++++++++++++++++++++- 2 files changed, 251 insertions(+), 214 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index af86c0f1..9684a034 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -3,19 +3,16 @@ import os import threading import warnings from datetime import timedelta, datetime -from hashlib import md5 -from io import BytesIO from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Lock from time import sleep -from . import helpers as utils, version -from .crypto import rsa, CdnDecrypter +from . import version +from .crypto import rsa from .errors import ( - RPCError, BrokenAuthKeyError, ServerError, - FloodWaitError, FloodTestPhoneWaitError, FileMigrateError, - TypeNotFoundError, UnauthorizedError, PhoneMigrateError, - NetworkMigrateError, UserMigrateError + RPCError, BrokenAuthKeyError, ServerError, FloodWaitError, + FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError, + PhoneMigrateError, NetworkMigrateError, UserMigrateError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode from .tl import TLObject, Session @@ -30,15 +27,8 @@ from .tl.functions.help import ( GetCdnConfigRequest, GetConfigRequest ) from .tl.functions.updates import GetStateRequest -from .tl.functions.upload import ( - GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest -) -from .tl.types import InputFile, InputFileBig from .tl.types.auth import ExportedAuthorization -from .tl.types.upload import FileCdnRedirect from .update_state import UpdateState -from .utils import get_appropriated_part_size - DEFAULT_DC_ID = 4 DEFAULT_IPV4_IP = '149.154.167.51' @@ -565,203 +555,6 @@ class TelegramBareClient: # endregion - # region Uploading media - - def upload_file(self, - file, - part_size_kb=None, - file_name=None, - allow_cache=True, - progress_callback=None): - """Uploads the specified file and returns a handle (an instance - of InputFile or InputFileBig, as required) which can be later used. - - Uploading a file will simply return a "handle" to the file stored - remotely in the Telegram servers, which can be later used on. This - will NOT upload the file to your own chat. - - 'file' may be either a file path, a byte array, or a stream. - Note that if the file is a stream it will need to be read - entirely into memory to tell its size first. - - If 'progress_callback' is not None, it should be a function that - takes two parameters, (bytes_uploaded, total_bytes). - - Default values for the optional parameters if left as None are: - part_size_kb = get_appropriated_part_size(file_size) - file_name = os.path.basename(file_path) - """ - if isinstance(file, (InputFile, InputFileBig)): - return file # Already uploaded - - if isinstance(file, str): - file_size = os.path.getsize(file) - elif isinstance(file, bytes): - file_size = len(file) - else: - file = file.read() - file_size = len(file) - - # File will now either be a string or bytes - if not part_size_kb: - part_size_kb = get_appropriated_part_size(file_size) - - if part_size_kb > 512: - raise ValueError('The part size must be less or equal to 512KB') - - part_size = int(part_size_kb * 1024) - if part_size % 1024 != 0: - raise ValueError('The part size must be evenly divisible by 1024') - - # Set a default file name if None was specified - file_id = utils.generate_random_long() - if not file_name: - if isinstance(file, str): - file_name = os.path.basename(file) - else: - file_name = str(file_id) - - # Determine whether the file is too big (over 10MB) or not - # Telegram does make a distinction between smaller or larger files - is_large = file_size > 10 * 1024 * 1024 - if not is_large: - # Calculate the MD5 hash before anything else. - # As this needs to be done always for small files, - # might as well do it before anything else and - # check the cache. - if isinstance(file, str): - with open(file, 'rb') as stream: - file = stream.read() - hash_md5 = md5(file) - else: - hash_md5 = None - - part_count = (file_size + part_size - 1) // part_size - __log__.info('Uploading file of %d bytes in %d chunks of %d', - file_size, part_count, part_size) - - with open(file, 'rb') if isinstance(file, str) else BytesIO(file) \ - as stream: - for part_index in range(part_count): - # Read the file by in chunks of size part_size - part = stream.read(part_size) - - # The SavePartRequest is different depending on whether - # the file is too large or not (over or less than 10MB) - if is_large: - request = SaveBigFilePartRequest(file_id, part_index, - part_count, part) - else: - request = SaveFilePartRequest(file_id, part_index, part) - - result = self(request) - if result: - __log__.debug('Uploaded %d/%d', part_index + 1, part_count) - if progress_callback: - progress_callback(stream.tell(), file_size) - else: - raise RuntimeError( - 'Failed to upload file part {}.'.format(part_index)) - - if is_large: - return InputFileBig(file_id, part_count, file_name) - else: - return InputFile(file_id, part_count, file_name, - md5_checksum=hash_md5.hexdigest()) - - # endregion - - # region Downloading media - - def download_file(self, - input_location, - file, - part_size_kb=None, - file_size=None, - progress_callback=None): - """Downloads the given InputFileLocation to file (a stream or str). - - If 'progress_callback' is not None, it should be a function that - takes two parameters, (bytes_downloaded, total_bytes). Note that - 'total_bytes' simply equals 'file_size', and may be None. - """ - if not part_size_kb: - if not file_size: - part_size_kb = 64 # Reasonable default - else: - part_size_kb = get_appropriated_part_size(file_size) - - part_size = int(part_size_kb * 1024) - # https://core.telegram.org/api/files says: - # > part_size % 1024 = 0 (divisible by 1KB) - # - # But https://core.telegram.org/cdn (more recent) says: - # > limit must be divisible by 4096 bytes - # So we just stick to the 4096 limit. - if part_size % 4096 != 0: - raise ValueError('The part size must be evenly divisible by 4096.') - - if isinstance(file, str): - # Ensure that we'll be able to download the media - utils.ensure_parent_dir_exists(file) - f = open(file, 'wb') - else: - f = file - - # The used client will change if FileMigrateError occurs - client = self - cdn_decrypter = None - - __log__.info('Downloading file in chunks of %d bytes', part_size) - try: - offset = 0 - while True: - try: - if cdn_decrypter: - result = cdn_decrypter.get_file() - else: - result = client(GetFileRequest( - input_location, offset, part_size - )) - - if isinstance(result, FileCdnRedirect): - __log__.info('File lives in a CDN') - cdn_decrypter, result = \ - CdnDecrypter.prepare_decrypter( - client, self._get_cdn_client(result), result - ) - - except FileMigrateError as e: - __log__.info('File lives in another DC') - client = self._get_exported_client(e.new_dc) - continue - - offset += part_size - - # If we have received no data (0 bytes), the file is over - # So there is nothing left to download and write - if not result.bytes: - # Return some extra information, unless it's a CDN file - return getattr(result, 'type', '') - - f.write(result.bytes) - __log__.debug('Saved %d more bytes', len(result.bytes)) - if progress_callback: - progress_callback(f.tell(), file_size) - finally: - if client != self: - client.disconnect() - - if cdn_decrypter: - try: - cdn_decrypter.client.disconnect() - except: - pass - if isinstance(file, str): - f.close() - - # endregion - # region Updates handling def sync_updates(self): diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 84c9beea..6e69e3f6 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,4 +1,6 @@ +import hashlib import itertools +import logging import os import sys import time @@ -6,6 +8,14 @@ from collections import OrderedDict, UserList from datetime import datetime, timedelta from mimetypes import guess_type +from io import BytesIO + +from telethon.crypto import CdnDecrypter +from telethon.tl.functions.upload import ( + SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest +) +from telethon.tl.types.upload import FileCdnRedirect + try: import socks except ImportError: @@ -16,7 +26,8 @@ from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, - SessionPasswordNeededError, FilePartMissingError) + SessionPasswordNeededError, FilePartMissingError, FileMigrateError +) from .network import ConnectionMode from .tl import TLObject from .tl.custom import Draft, Dialog @@ -55,11 +66,13 @@ from .tl.types import ( UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, - InputSingleMedia, InputMediaPhoto, InputPhoto + InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig ) from .tl.types.messages import DialogsSlice from .extensions import markdown +__log__ = logging.getLogger(__name__) + class TelegramClient(TelegramBareClient): """ @@ -1011,6 +1024,130 @@ class TelegramClient(TelegramBareClient): progress_callback=progress_callback, reply_to=reply_to ) + def upload_file(self, + file, + part_size_kb=None, + file_name=None, + allow_cache=True, + progress_callback=None): + """ + Uploads the specified file and returns a handle (an instance of + InputFile or InputFileBig, as required) which can be later used + before it expires (they are usable during less than a day). + + Uploading a file will simply return a "handle" to the file stored + remotely in the Telegram servers, which can be later used on. This + will **not** upload the file to your own chat or any chat at all. + + Args: + file (:obj:`str` | :obj:`bytes` | :obj:`file`): + The path of the file, byte array, or stream that will be sent. + 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". + + Subsequent calls with the very same file will result in + immediate uploads, unless ``.clear_file_cache()`` is called. + + part_size_kb (:obj:`int`, optional): + Chunk size when uploading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_name (:obj:`str`, optional): + The file name which will be used on the resulting InputFile. + If not specified, the name will be taken from the ``file`` + and if this is not a ``str``, it will be ``"unnamed"``. + + allow_cache (:obj:`bool`, optional): + Whether to allow reusing the file from cache or not. Unused. + + progress_callback (:obj:`callable`, optional): + A callback function accepting two parameters: + ``(sent bytes, total)``. + + Returns: + The InputFile (or InputFileBig if >10MB). + """ + if isinstance(file, (InputFile, InputFileBig)): + return file # Already uploaded + + if isinstance(file, str): + file_size = os.path.getsize(file) + elif isinstance(file, bytes): + file_size = len(file) + else: + file = file.read() + file_size = len(file) + + # File will now either be a string or bytes + if not part_size_kb: + part_size_kb = utils.get_appropriated_part_size(file_size) + + if part_size_kb > 512: + raise ValueError('The part size must be less or equal to 512KB') + + part_size = int(part_size_kb * 1024) + if part_size % 1024 != 0: + raise ValueError( + 'The part size must be evenly divisible by 1024') + + # Set a default file name if None was specified + file_id = helpers.generate_random_long() + if not file_name: + if isinstance(file, str): + file_name = os.path.basename(file) + else: + file_name = str(file_id) + + # Determine whether the file is too big (over 10MB) or not + # Telegram does make a distinction between smaller or larger files + is_large = file_size > 10 * 1024 * 1024 + if not is_large: + # Calculate the MD5 hash before anything else. + # As this needs to be done always for small files, + # might as well do it before anything else and + # check the cache. + if isinstance(file, str): + with open(file, 'rb') as stream: + file = stream.read() + hash_md5 = hashlib.md5(file) + else: + hash_md5 = None + + part_count = (file_size + part_size - 1) // part_size + __log__.info('Uploading file of %d bytes in %d chunks of %d', + file_size, part_count, part_size) + + with open(file, 'rb') if isinstance(file, str) else BytesIO(file) \ + as stream: + for part_index in range(part_count): + # Read the file by in chunks of size part_size + part = stream.read(part_size) + + # The SavePartRequest is different depending on whether + # the file is too large or not (over or less than 10MB) + if is_large: + request = SaveBigFilePartRequest(file_id, part_index, + part_count, part) + else: + request = SaveFilePartRequest(file_id, part_index, part) + + result = self(request) + if result: + __log__.debug('Uploaded %d/%d', part_index + 1, + part_count) + if progress_callback: + progress_callback(stream.tell(), file_size) + else: + raise RuntimeError( + 'Failed to upload file part {}.'.format(part_index)) + + if is_large: + return InputFileBig(file_id, part_count, file_name) + else: + return InputFile(file_id, part_count, file_name, + md5_checksum=hash_md5.hexdigest()) + # endregion # region Downloading media requests @@ -1292,6 +1429,113 @@ class TelegramClient(TelegramBareClient): return result i += 1 + def download_file(self, + input_location, + file, + part_size_kb=None, + file_size=None, + progress_callback=None): + """ + Downloads the given input location to a file. + + Args: + input_location (:obj:`InputFileLocation`): + The file location from which the file will be downloaded. + + file (:obj:`str` | :obj:`file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + + part_size_kb (:obj:`int`, optional): + Chunk size when downloading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_size (:obj:`int`, optional): + The file size that is about to be downloaded, if known. + Only used if ``progress_callback`` is specified. + + progress_callback (:obj:`callable`, optional): + A callback function accepting two parameters: + ``(downloaded bytes, total)``. Note that the + ``total`` is the provided ``file_size``. + """ + if not part_size_kb: + if not file_size: + part_size_kb = 64 # Reasonable default + else: + part_size_kb = utils.get_appropriated_part_size(file_size) + + part_size = int(part_size_kb * 1024) + # https://core.telegram.org/api/files says: + # > part_size % 1024 = 0 (divisible by 1KB) + # + # But https://core.telegram.org/cdn (more recent) says: + # > limit must be divisible by 4096 bytes + # So we just stick to the 4096 limit. + if part_size % 4096 != 0: + raise ValueError( + 'The part size must be evenly divisible by 4096.') + + if isinstance(file, str): + # Ensure that we'll be able to download the media + helpers.ensure_parent_dir_exists(file) + f = open(file, 'wb') + else: + f = file + + # The used client will change if FileMigrateError occurs + client = self + cdn_decrypter = None + + __log__.info('Downloading file in chunks of %d bytes', part_size) + try: + offset = 0 + while True: + try: + if cdn_decrypter: + result = cdn_decrypter.get_file() + else: + result = client(GetFileRequest( + input_location, offset, part_size + )) + + if isinstance(result, FileCdnRedirect): + __log__.info('File lives in a CDN') + cdn_decrypter, result = \ + CdnDecrypter.prepare_decrypter( + client, self._get_cdn_client(result), + result + ) + + except FileMigrateError as e: + __log__.info('File lives in another DC') + client = self._get_exported_client(e.new_dc) + continue + + offset += part_size + + # If we have received no data (0 bytes), the file is over + # So there is nothing left to download and write + if not result.bytes: + # Return some extra information, unless it's a CDN file + return getattr(result, 'type', '') + + f.write(result.bytes) + __log__.debug('Saved %d more bytes', len(result.bytes)) + if progress_callback: + progress_callback(f.tell(), file_size) + finally: + if client != self: + client.disconnect() + + if cdn_decrypter: + try: + cdn_decrypter.client.disconnect() + except: + pass + if isinstance(file, str): + f.close() + # endregion # endregion From 7e707dbbd991ba22cdf70a1346683837ba970a2f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 19:35:46 +0100 Subject: [PATCH 099/361] Fix using enum on sqlite instead its value --- telethon/tl/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 5d89a5f7..e2c653d4 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -445,7 +445,7 @@ class Session: tuple_ = self._conn.execute( 'select id, hash from sent_files ' 'where md5_digest = ? and file_size = ? and type = ?', - (md5_digest, file_size, _SentFileType.from_type(cls)) + (md5_digest, file_size, _SentFileType.from_type(cls).value) ).fetchone() if tuple_: # Both allowed classes have (id, access_hash) as parameters @@ -459,7 +459,7 @@ class Session: self._conn.execute( 'insert into sent_files values (?,?,?,?,?)', ( md5_digest, file_size, - _SentFileType.from_type(type(instance)), + _SentFileType.from_type(type(instance)).value, instance.id, instance.access_hash )) self.save() From 0e4611a593dd805c05a4420aced410166504c57c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 19:36:47 +0100 Subject: [PATCH 100/361] Properly implement InputPhoto/InputDocument caching Since uploading a file is done on the TelegramClient, and the InputFiles are only valid for a short period of time, it only makes sense to cache the sent media instead (which should not expire). The problem is the MD5 is only needed when uploading the file. The solution is to allow this method to check for the wanted cache, and if available, return an instance of that, so to preserve the flexibility of both options (always InputFile, or the cached InputPhoto/InputDocument) instead reuploading. --- telethon/telegram_client.py | 146 +++++++++++++++++++++--------------- telethon/tl/session.py | 2 +- 2 files changed, 85 insertions(+), 63 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 6e69e3f6..6e249cc4 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -66,7 +66,8 @@ from .tl.types import ( UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, - InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig + InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, + InputDocument, InputMediaDocument ) from .tl.types.messages import DialogsSlice from .extensions import markdown @@ -875,7 +876,9 @@ class TelegramClient(TelegramBareClient): allow_cache (:obj:`bool`, optional): Whether to allow using the cached version stored in the - database or not. Defaults to ``True`` to avoid reuploads. + database or not. Defaults to ``True`` to avoid re-uploads. + Must be ``False`` if you wish to use different attributes + or thumb than those that were used when the file was cached. Kwargs: If "is_voice_note" in kwargs, despite its value, and the file is @@ -892,8 +895,7 @@ class TelegramClient(TelegramBareClient): if all(utils.is_image(x) for x in file): return self._send_album( entity, file, caption=caption, - progress_callback=progress_callback, reply_to=reply_to, - allow_cache=allow_cache + progress_callback=progress_callback, reply_to=reply_to ) # Not all are images, so send all the files one by one return [ @@ -905,10 +907,20 @@ class TelegramClient(TelegramBareClient): ) for x in file ] + as_image = utils.is_image(file) and not force_document + use_cache = InputPhoto if as_image else InputDocument file_handle = self.upload_file( - file, progress_callback=progress_callback, allow_cache=allow_cache) + file, progress_callback=progress_callback, + use_cache=use_cache if allow_cache else None + ) - if utils.is_image(file) and not force_document: + if isinstance(file_handle, use_cache): + # File was cached, so an instance of use_cache was returned + if as_image: + media = InputMediaPhoto(file_handle, caption) + else: + media = InputMediaDocument(file_handle, caption) + elif as_image: media = InputMediaUploadedPhoto(file_handle, caption) else: mime_type = None @@ -964,19 +976,19 @@ class TelegramClient(TelegramBareClient): media=media, reply_to_msg_id=self._get_reply_to(reply_to) ) - try: - return self._get_response_message(request, self(request)) - except FilePartMissingError: - # After a while, cached files are invalidated and this - # error is raised. The file needs to be uploaded again. - if not allow_cache: - raise - return self.send_file( - entity, file, allow_cache=False, - caption=caption, force_document=force_document, - progress_callback=progress_callback, reply_to=reply_to, - attributes=attributes, thumb=thumb, **kwargs - ) + msg = self._get_response_message(request, self(request)) + if msg and isinstance(file_handle, InputFile): + # There was a response message and we didn't use cached + # version, so cache whatever we just sent to the database. + # Note that the InputFile was modified to have md5/size. + md5, size = file_handle.md5, file_handle.size + if as_image: + to_cache = utils.get_input_photo(msg.media.photo) + else: + to_cache = utils.get_input_document(msg.media.document) + self.session.cache_file(md5, size, to_cache) + + return msg def send_voice_note(self, entity, file, caption='', progress_callback=None, reply_to=None): @@ -987,48 +999,44 @@ class TelegramClient(TelegramBareClient): is_voice_note=()) # empty tuple is enough def _send_album(self, entity, files, caption='', - progress_callback=None, reply_to=None, - allow_cache=True): + progress_callback=None, reply_to=None): """Specialized version of .send_file for albums""" + # We don't care if the user wants to avoid cache, we will use it + # anyway. Why? The cached version will be exactly the same thing + # we need to produce right now to send albums (uploadMedia), and + # cache only makes a difference for documents where the user may + # want the attributes used on them to change. Caption's ignored. entity = self.get_input_entity(entity) reply_to = self._get_reply_to(reply_to) - try: - # Need to upload the media first - media = [ - self(UploadMediaRequest(entity, InputMediaUploadedPhoto( - self.upload_file(file, allow_cache=allow_cache), - caption=caption - ))) - for file in files - ] - # Now we can construct the multi-media request - result = self(SendMultiMediaRequest( - entity, reply_to_msg_id=reply_to, multi_media=[ - InputSingleMedia(InputMediaPhoto( - InputPhoto(m.photo.id, m.photo.access_hash), - caption=caption - )) - for m in media - ] - )) - return [ - self._get_response_message(update.id, result) - for update in result.updates - if isinstance(update, UpdateMessageID) - ] - except FilePartMissingError: - if not allow_cache: - raise - return self._send_album( - entity, files, allow_cache=False, caption=caption, - progress_callback=progress_callback, reply_to=reply_to - ) + + # Need to upload the media first, but only if they're not cached yet + media = [] + for file in files: + # fh will either be InputPhoto or a modified InputFile + fh = self.upload_file(file, use_cache=InputPhoto) + if not isinstance(fh, InputPhoto): + input_photo = utils.get_input_photo(self(UploadMediaRequest( + entity, media=InputMediaUploadedPhoto(fh, caption) + )).photo) + self.session.cache_file(fh.md5, fh.size, input_photo) + fh = input_photo + media.append(InputSingleMedia(InputMediaPhoto(fh, caption))) + + # Now we can construct the multi-media request + result = self(SendMultiMediaRequest( + entity, reply_to_msg_id=reply_to, multi_media=media + )) + return [ + self._get_response_message(update.id, result) + for update in result.updates + if isinstance(update, UpdateMessageID) + ] def upload_file(self, file, part_size_kb=None, file_name=None, - allow_cache=True, + use_cache=None, progress_callback=None): """ Uploads the specified file and returns a handle (an instance of @@ -1058,15 +1066,20 @@ class TelegramClient(TelegramBareClient): If not specified, the name will be taken from the ``file`` and if this is not a ``str``, it will be ``"unnamed"``. - allow_cache (:obj:`bool`, optional): - Whether to allow reusing the file from cache or not. Unused. + use_cache (:obj:`type`, optional): + The type of cache to use (currently either ``InputDocument`` + or ``InputPhoto``). If present and the file is small enough + to need the MD5, it will be checked against the database, + and if a match is found, the upload won't be made. Instead, + an instance of type ``use_cache`` will be returned. progress_callback (:obj:`callable`, optional): A callback function accepting two parameters: ``(sent bytes, total)``. Returns: - The InputFile (or InputFileBig if >10MB). + The InputFile (or InputFileBig if >10MB) with two extra + attributes: ``.md5`` (its ``.digest()``) and ``size``. """ if isinstance(file, (InputFile, InputFileBig)): return file # Already uploaded @@ -1102,6 +1115,7 @@ class TelegramClient(TelegramBareClient): # Determine whether the file is too big (over 10MB) or not # Telegram does make a distinction between smaller or larger files is_large = file_size > 10 * 1024 * 1024 + hash_md5 = hashlib.md5() if not is_large: # Calculate the MD5 hash before anything else. # As this needs to be done always for small files, @@ -1110,9 +1124,13 @@ class TelegramClient(TelegramBareClient): if isinstance(file, str): with open(file, 'rb') as stream: file = stream.read() - hash_md5 = hashlib.md5(file) - else: - hash_md5 = None + hash_md5.update(file) + if use_cache: + cached = self.session.get_file( + hash_md5.digest(), file_size, cls=use_cache + ) + if cached: + return cached part_count = (file_size + part_size - 1) // part_size __log__.info('Uploading file of %d bytes in %d chunks of %d', @@ -1143,10 +1161,14 @@ class TelegramClient(TelegramBareClient): 'Failed to upload file part {}.'.format(part_index)) if is_large: - return InputFileBig(file_id, part_count, file_name) + result = InputFileBig(file_id, part_count, file_name) else: - return InputFile(file_id, part_count, file_name, - md5_checksum=hash_md5.hexdigest()) + result = InputFile(file_id, part_count, file_name, + md5_checksum=hash_md5.hexdigest()) + + result.md5 = hash_md5.digest() + result.size = file_size + return result # endregion diff --git a/telethon/tl/session.py b/telethon/tl/session.py index e2c653d4..bfed1a79 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -457,7 +457,7 @@ class Session: with self._db_lock: self._conn.execute( - 'insert into sent_files values (?,?,?,?,?)', ( + 'insert or replace into sent_files values (?,?,?,?,?)', ( md5_digest, file_size, _SentFileType.from_type(type(instance)).value, instance.id, instance.access_hash From b546c022109429d061dd6482fde928721252944e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 20:08:05 +0100 Subject: [PATCH 101/361] Return a custom class for sized InputFile instead extra attrs --- telethon/telegram_client.py | 32 +++++++++++--------------- telethon/tl/custom/__init__.py | 1 + telethon/tl/custom/input_sized_file.py | 9 ++++++++ 3 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 telethon/tl/custom/input_sized_file.py diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 6e249cc4..5c4493f3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -6,15 +6,15 @@ import sys import time from collections import OrderedDict, UserList from datetime import datetime, timedelta +from io import BytesIO from mimetypes import guess_type -from io import BytesIO - -from telethon.crypto import CdnDecrypter -from telethon.tl.functions.upload import ( - SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest +from .crypto import CdnDecrypter +from .tl.custom import InputSizedFile +from .tl.functions.upload import ( + SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest ) -from telethon.tl.types.upload import FileCdnRedirect +from .tl.types.upload import FileCdnRedirect try: import socks @@ -26,7 +26,7 @@ from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, - SessionPasswordNeededError, FilePartMissingError, FileMigrateError + SessionPasswordNeededError, FileMigrateError ) from .network import ConnectionMode from .tl import TLObject @@ -977,10 +977,9 @@ class TelegramClient(TelegramBareClient): reply_to_msg_id=self._get_reply_to(reply_to) ) msg = self._get_response_message(request, self(request)) - if msg and isinstance(file_handle, InputFile): + if msg and isinstance(file_handle, InputSizedFile): # There was a response message and we didn't use cached # version, so cache whatever we just sent to the database. - # Note that the InputFile was modified to have md5/size. md5, size = file_handle.md5, file_handle.size if as_image: to_cache = utils.get_input_photo(msg.media.photo) @@ -1078,8 +1077,8 @@ class TelegramClient(TelegramBareClient): ``(sent bytes, total)``. Returns: - The InputFile (or InputFileBig if >10MB) with two extra - attributes: ``.md5`` (its ``.digest()``) and ``size``. + ``InputFileBig`` if the file size is larger than 10MB, + ``InputSizedFile`` (subclass of ``InputFile``) otherwise. """ if isinstance(file, (InputFile, InputFileBig)): return file # Already uploaded @@ -1161,14 +1160,11 @@ class TelegramClient(TelegramBareClient): 'Failed to upload file part {}.'.format(part_index)) if is_large: - result = InputFileBig(file_id, part_count, file_name) + return InputFileBig(file_id, part_count, file_name) else: - result = InputFile(file_id, part_count, file_name, - md5_checksum=hash_md5.hexdigest()) - - result.md5 = hash_md5.digest() - result.size = file_size - return result + return InputSizedFile( + file_id, part_count, file_name, md5=hash_md5, size=file_size + ) # endregion diff --git a/telethon/tl/custom/__init__.py b/telethon/tl/custom/__init__.py index 5b6bf44d..f74189f6 100644 --- a/telethon/tl/custom/__init__.py +++ b/telethon/tl/custom/__init__.py @@ -1,2 +1,3 @@ from .draft import Draft from .dialog import Dialog +from .input_sized_file import InputSizedFile diff --git a/telethon/tl/custom/input_sized_file.py b/telethon/tl/custom/input_sized_file.py new file mode 100644 index 00000000..fcb743f6 --- /dev/null +++ b/telethon/tl/custom/input_sized_file.py @@ -0,0 +1,9 @@ +from ..types import InputFile + + +class InputSizedFile(InputFile): + """InputFile class with two extra parameters: md5 (digest) and size""" + def __init__(self, id_, parts, name, md5, size): + super().__init__(id_, parts, name, md5.hexdigest()) + self.md5 = md5.digest() + self.size = size From 1c9fa76edeedc6d17325216cab9c574a23ca0caa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 11:47:45 +0100 Subject: [PATCH 102/361] Add new method to .resolve() parameters instead on init TLObject's __init__ used to call utils.get_input_* methods and similar to auto-cast things like User into InputPeerUser as required. Now there's a custom .resolve() method for this purpose with several advantages: - Old behaviour still works, autocasts work like usual. - A request can be constructed and later modified, before the autocast only occured on the constructor but now while invoking. - This allows us to not only use the utils module but also the client, so it's even possible to use usernames or phone numbers for things that require an InputPeer. This actually assumes the TelegramClient subclass is being used and not the bare version which would fail when calling .get_input_peer(). --- telethon/telegram_bare_client.py | 5 +- telethon/tl/tlobject.py | 3 + telethon_generator/tl_generator.py | 104 +++++++++++++++-------------- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 9684a034..ba6ae374 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -7,7 +7,7 @@ from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Lock from time import sleep -from . import version +from . import version, utils from .crypto import rsa from .errors import ( RPCError, BrokenAuthKeyError, ServerError, FloodWaitError, @@ -420,6 +420,9 @@ class TelegramBareClient: if self._background_error: raise self._background_error + for request in requests: + request.resolve(self, utils) + # For logging purposes if len(requests) == 1: which = type(requests[0]).__name__ diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index ad930f9c..7c86a24a 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -144,6 +144,9 @@ class TLObject: raise TypeError('Cannot interpret "{}" as a date.'.format(dt)) # These should be overrode + def resolve(self, client, utils): + pass + def to_dict(self, recursive=True): return {} diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 3116003a..39bad15f 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -10,6 +10,15 @@ AUTO_GEN_NOTICE = \ '"""File generated by TLObjects\' generator. All changes will be ERASED"""' +AUTO_CASTS = { + 'InputPeer': 'utils.get_input_peer(client.get_input_entity({}))', + 'InputChannel': 'utils.get_input_channel(client.get_input_entity({}))', + 'InputUser': 'utils.get_input_user(client.get_input_entity({}))', + 'InputMedia': 'utils.get_input_media({})', + 'InputPhoto': 'utils.get_input_photo({})' +} + + class TLGenerator: def __init__(self, output_dir): self.output_dir = output_dir @@ -257,10 +266,45 @@ class TLGenerator: builder.writeln() for arg in args: - TLGenerator._write_self_assigns(builder, tlobject, arg, args) + if not arg.can_be_inferred: + builder.writeln('self.{0} = {0}'.format(arg.name)) + continue + + # Currently the only argument that can be + # inferred are those called 'random_id' + if arg.name == 'random_id': + # Endianness doesn't really matter, and 'big' is shorter + code = "int.from_bytes(os.urandom({}), 'big', signed=True)" \ + .format(8 if arg.type == 'long' else 4) + + if arg.is_vector: + # Currently for the case of "messages.forwardMessages" + # Ensure we can infer the length from id:Vector<> + if not next( + a for a in args if a.name == 'id').is_vector: + raise ValueError( + 'Cannot infer list of random ids for ', tlobject + ) + code = '[{} for _ in range(len(id))]'.format(code) + + builder.writeln( + "self.random_id = random_id if random_id " + "is not None else {}".format(code) + ) + else: + raise ValueError('Cannot infer a value for ', arg) builder.end_block() + # Write the resolve(self, client, utils) method + if any(arg.type in AUTO_CASTS for arg in args): + builder.writeln('def resolve(self, client, utils):') + for arg in args: + ac = AUTO_CASTS.get(arg.type, None) + if ac: + TLGenerator._write_self_assign(builder, arg, ac) + builder.end_block() + # Write the to_dict(self) method builder.writeln('def to_dict(self, recursive=True):') if args: @@ -370,59 +414,17 @@ class TLGenerator: # builder.end_block() # No need to end the last block @staticmethod - def _write_self_assigns(builder, tlobject, arg, args): - if arg.can_be_inferred: - # Currently the only argument that can be - # inferred are those called 'random_id' - if arg.name == 'random_id': - # Endianness doesn't really matter, and 'big' is shorter - code = "int.from_bytes(os.urandom({}), 'big', signed=True)"\ - .format(8 if arg.type == 'long' else 4) - - if arg.is_vector: - # Currently for the case of "messages.forwardMessages" - # Ensure we can infer the length from id:Vector<> - if not next(a for a in args if a.name == 'id').is_vector: - raise ValueError( - 'Cannot infer list of random ids for ', tlobject - ) - code = '[{} for _ in range(len(id))]'.format(code) - - builder.writeln( - "self.random_id = random_id if random_id " - "is not None else {}".format(code) - ) - else: - raise ValueError('Cannot infer a value for ', arg) - - # Well-known cases, auto-cast it to the right type - elif arg.type == 'InputPeer' and tlobject.is_function: - TLGenerator.write_get_input(builder, arg, 'get_input_peer') - elif arg.type == 'InputChannel' and tlobject.is_function: - TLGenerator.write_get_input(builder, arg, 'get_input_channel') - elif arg.type == 'InputUser' and tlobject.is_function: - 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)) - - @staticmethod - def write_get_input(builder, arg, get_input_code): - """Returns "True" if the get_input_* code was written when assigning - a parameter upon creating the request. Returns False otherwise - """ + def _write_self_assign(builder, arg, get_input_code): + """Writes self.arg = input.format(self.arg), considering vectors""" if arg.is_vector: - builder.write('self.{0} = [{1}(_x) for _x in {0}]' - .format(arg.name, get_input_code)) + builder.write('self.{0} = [{1} for _x in self.{0}]' + .format(arg.name, get_input_code.format('_x'))) else: - builder.write('self.{0} = {1}({0})' - .format(arg.name, get_input_code)) + builder.write('self.{} = {}'.format( + arg.name, get_input_code.format('self.' + arg.name))) + builder.writeln( - ' if {} else None'.format(arg.name) if arg.is_flag else '' + ' if self.{} else None'.format(arg.name) if arg.is_flag else '' ) @staticmethod From f6d98a61cfe4decb2900671ad84e994d19583309 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 11:52:44 +0100 Subject: [PATCH 103/361] Add stub .get_input_entity() to TelegramBareClient .resolve() calls should now work even if the subclass isn't in use. --- telethon/telegram_bare_client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index ba6ae374..bee3ecdd 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -556,6 +556,13 @@ class TelegramBareClient: (code request sent and confirmed)?""" return self._authorized + def get_input_entity(self, peer): + """ + Stub method, no functionality so that calling + ``.get_input_entity()`` from ``.resolve()`` doesn't fail. + """ + return peer + # endregion # region Updates handling From 33e50aaee1a7c599ffbfd3aabd463e6b3b9a9675 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 12:12:52 +0100 Subject: [PATCH 104/361] Reuse .on_response/.__str__/.stringify, override iff necessary --- telethon/extensions/binary_reader.py | 2 ++ telethon/tl/tlobject.py | 11 ++++++++++ telethon_generator/tl_generator.py | 33 +++++++++++++++++----------- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 1402083f..ecf7dd1b 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -133,6 +133,8 @@ class BinaryReader: return True elif value == 0xbc799737: # boolFalse return False + elif value == 0x1cb5c415: # Vector + return [self.tgread_object() for _ in range(self.read_int())] # If there was still no luck, give up self.seek(-4) # Go back diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 7c86a24a..ac0b65f8 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -7,6 +7,7 @@ class TLObject: def __init__(self): self.confirm_received = Event() self.rpc_error = None + self.result = None # These should be overrode self.content_related = False # Only requests/functions/queries are @@ -143,6 +144,16 @@ class TLObject: raise TypeError('Cannot interpret "{}" as a date.'.format(dt)) + # These are nearly always the same for all subclasses + def on_response(self, reader): + self.result = reader.tgread_object() + + def __str__(self): + return TLObject.pretty_format(self) + + def stringify(self): + return TLObject.pretty_format(self, indent=0) + # These should be overrode def resolve(self, client, utils): pass diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 39bad15f..fb8ca4bd 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -395,23 +395,30 @@ class TLGenerator: if not a.flag_indicator and not a.generic_definition ) )) - builder.end_block() # 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: + # + # The default behaviour is reading a TLObject too, so no need + # to override it unless necessary. + if tlobject.is_function and not TLGenerator._is_boxed(tlobject.result): + builder.end_block() builder.writeln('def on_response(self, reader):') TLGenerator.write_request_result_code(builder, tlobject) - builder.end_block() - # Write the __str__(self) and stringify(self) functions - builder.writeln('def __str__(self):') - builder.writeln('return TLObject.pretty_format(self)') - builder.end_block() - - builder.writeln('def stringify(self):') - builder.writeln('return TLObject.pretty_format(self, indent=0)') - # builder.end_block() # No need to end the last block + @staticmethod + def _is_boxed(type_): + # https://core.telegram.org/mtproto/serialize#boxed-and-bare-types + # TL;DR; boxed types start with uppercase always, so we can use + # this to check whether everything in it is boxed or not. + # + # The API always returns a boxed type, but it may inside a Vector<> + # or a namespace, and the Vector may have a not-boxed type. For this + # reason we find whatever index, '<' or '.'. If neither are present + # we will get -1, and the 0th char is always upper case thus works. + # For Vector types and namespaces, it will check in the right place. + check_after = max(type_.find('<'), type_.find('.')) + return type_[check_after + 1].isupper() @staticmethod def _write_self_assign(builder, arg, get_input_code): @@ -697,13 +704,13 @@ class TLGenerator: # not parsed as arguments are and it's a bit harder to tell which # is which. if tlobject.result == 'Vector': - builder.writeln('reader.read_int() # Vector id') + builder.writeln('reader.read_int() # Vector ID') builder.writeln('count = reader.read_int()') builder.writeln( 'self.result = [reader.read_int() for _ in range(count)]' ) elif tlobject.result == 'Vector': - builder.writeln('reader.read_int() # Vector id') + builder.writeln('reader.read_int() # Vector ID') builder.writeln('count = reader.read_long()') builder.writeln( 'self.result = [reader.read_long() for _ in range(count)]' From e3c56b0d98d6862d6fe03071f526652bd662d2b3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 13:00:17 +0100 Subject: [PATCH 105/361] Reduce autocast overhead as much as possible Rationale: if the user is doing things right, the penalty for being friendly (i.e. autocasting to the right version, like User -> InputPeerUser), should be as little as possible. Removing the redundant type() call to access .SUBCLASS_OF_ID and assuming the user provided a TLObject (through excepting whenever the attribute is not available) is x2 and x4 times faster respectively. Of course, this is a micro-optimization, but I still consider it's good to benefit users doing things right or avoiding redundant calls. --- telethon/telegram_client.py | 32 ++++++++------- telethon/utils.py | 77 ++++++++++++++++++------------------- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5c4493f3..f09a62fa 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -818,10 +818,12 @@ class TelegramClient(TelegramBareClient): if isinstance(reply_to, int): return reply_to - if isinstance(reply_to, TLObject) and \ - type(reply_to).SUBCLASS_OF_ID == 0x790009e3: - # hex(crc32(b'Message')) = 0x790009e3 - return reply_to.id + try: + if reply_to.SUBCLASS_OF_ID == 0x790009e3: + # hex(crc32(b'Message')) = 0x790009e3 + return reply_to.id + except AttributeError: + pass raise TypeError('Invalid reply_to type: {}'.format(type(reply_to))) @@ -1191,9 +1193,14 @@ class TelegramClient(TelegramBareClient): """ photo = entity possible_names = [] - if not isinstance(entity, TLObject) or type(entity).SUBCLASS_OF_ID in ( + try: + is_entity = entity.SUBCLASS_OF_ID in ( 0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697 - ): + ) + except AttributeError: + return None # Not even a TLObject as attribute access failed + + if is_entity: # Maybe it is an user or a chat? Or their full versions? # # The hexadecimal numbers above are simply: @@ -1705,14 +1712,13 @@ class TelegramClient(TelegramBareClient): if isinstance(peer, int): peer = PeerUser(peer) is_peer = True - - elif isinstance(peer, TLObject): - is_peer = type(peer).SUBCLASS_OF_ID == 0x2d45687 # crc32(b'Peer') - if not is_peer: - try: + else: + try: + is_peer = peer.SUBCLASS_OF_ID == 0x2d45687 # crc32(b'Peer') + if not is_peer: return utils.get_input_peer(peer) - except TypeError: - pass + except (AttributeError, TypeError): + pass # Attribute if not TLObject, Type if not "casteable" if not is_peer: raise TypeError( diff --git a/telethon/utils.py b/telethon/utils.py index 8549e18d..a4850d4c 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -3,11 +3,9 @@ Utilities for working with the Telegram API itself (such as handy methods to convert between an entity like an User, Chat, etc. into its Input version) """ import math +import re from mimetypes import add_type, guess_extension -import re - -from .tl import TLObject from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty, @@ -25,7 +23,6 @@ from .tl.types import ( InputMediaUploadedPhoto, DocumentAttributeFilename, photos ) - USERNAME_RE = re.compile( r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' ) @@ -81,12 +78,12 @@ def _raise_cast_fail(entity, target): def get_input_peer(entity, allow_self=True): """Gets the input peer for the given "entity" (user, chat or channel). A TypeError is raised if the given entity isn't a supported type.""" - if not isinstance(entity, TLObject): + try: + if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') + return entity + except AttributeError: _raise_cast_fail(entity, 'InputPeer') - if type(entity).SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') - return entity - if isinstance(entity, User): if entity.is_self and allow_self: return InputPeerSelf() @@ -123,12 +120,12 @@ def get_input_peer(entity, allow_self=True): def get_input_channel(entity): """Similar to get_input_peer, but for InputChannel's alone""" - if not isinstance(entity, TLObject): + try: + if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') + return entity + except AttributeError: _raise_cast_fail(entity, 'InputChannel') - if type(entity).SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') - return entity - if isinstance(entity, (Channel, ChannelForbidden)): return InputChannel(entity.id, entity.access_hash or 0) @@ -140,12 +137,12 @@ def get_input_channel(entity): def get_input_user(entity): """Similar to get_input_peer, but for InputUser's alone""" - if not isinstance(entity, TLObject): + try: + if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'): + return entity + except AttributeError: _raise_cast_fail(entity, 'InputUser') - if type(entity).SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser') - return entity - if isinstance(entity, User): if entity.is_self: return InputUserSelf() @@ -169,12 +166,12 @@ def get_input_user(entity): def get_input_document(document): """Similar to get_input_peer, but for documents""" - if not isinstance(document, TLObject): + try: + if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'): + return document + except AttributeError: _raise_cast_fail(document, 'InputDocument') - if type(document).SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument') - return document - if isinstance(document, Document): return InputDocument(id=document.id, access_hash=document.access_hash) @@ -192,12 +189,12 @@ def get_input_document(document): def get_input_photo(photo): """Similar to get_input_peer, but for documents""" - if not isinstance(photo, TLObject): + try: + if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'): + return photo + except AttributeError: _raise_cast_fail(photo, 'InputPhoto') - if type(photo).SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto') - return photo - if isinstance(photo, photos.Photo): photo = photo.photo @@ -212,12 +209,12 @@ def get_input_photo(photo): def get_input_geo(geo): """Similar to get_input_peer, but for geo points""" - if not isinstance(geo, TLObject): + try: + if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'): + return geo + except AttributeError: _raise_cast_fail(geo, 'InputGeoPoint') - if type(geo).SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint') - return geo - if isinstance(geo, GeoPoint): return InputGeoPoint(lat=geo.lat, long=geo.long) @@ -239,12 +236,12 @@ def get_input_media(media, user_caption=None, is_photo=False): If the media is a file location and is_photo is known to be True, it will be treated as an InputMediaUploadedPhoto. """ - if not isinstance(media, TLObject): + try: + if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia'): + return media + except AttributeError: _raise_cast_fail(media, 'InputMedia') - if type(media).SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia') - return media - if isinstance(media, MessageMediaPhoto): return InputMediaPhoto( id=get_input_photo(media.photo), @@ -357,15 +354,15 @@ def get_peer_id(peer): a call to utils.resolve_id(marked_id). """ # First we assert it's a Peer TLObject, or early return for integers - if not isinstance(peer, TLObject): - if isinstance(peer, int): - return peer - else: - _raise_cast_fail(peer, 'int') + if isinstance(peer, int): + return peer - elif type(peer).SUBCLASS_OF_ID not in {0x2d45687, 0xc91c90b6}: - # Not a Peer or an InputPeer, so first get its Input version - peer = get_input_peer(peer, allow_self=False) + try: + if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): + # Not a Peer or an InputPeer, so first get its Input version + peer = get_input_peer(peer, allow_self=False) + except AttributeError: + _raise_cast_fail(peer, 'int') # Set the right ID/kind, or raise if the TLObject is not recognised if isinstance(peer, (PeerUser, InputPeerUser)): From 0e43022959c94faafc06482d1da93663c46b3b8b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 13:40:04 +0100 Subject: [PATCH 106/361] Remove redundant import, show type instead TLObject on docstring --- telethon_generator/parser/tl_object.py | 2 +- telethon_generator/tl_generator.py | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/telethon_generator/parser/tl_object.py b/telethon_generator/parser/tl_object.py index 278a66eb..034cb3c3 100644 --- a/telethon_generator/parser/tl_object.py +++ b/telethon_generator/parser/tl_object.py @@ -264,7 +264,7 @@ class TLArg: 'date': 'datetime.datetime | None', # None date = 0 timestamp 'bytes': 'bytes', 'true': 'bool', - }.get(self.type, 'TLObject') + }.get(self.type, self.type) if self.is_vector: result = 'list[{}]'.format(result) if self.is_flag and self.type != 'date': diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index fb8ca4bd..18293ba1 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -146,15 +146,6 @@ class TLGenerator: x for x in namespace_tlobjects.keys() if x ))) - # Import 'get_input_*' utils - # TODO Support them on types too - if 'functions' in out_dir: - builder.writeln( - 'from {}.utils import get_input_peer, ' - 'get_input_channel, get_input_user, ' - 'get_input_media, get_input_photo'.format('.' * depth) - ) - # Import 'os' for those needing access to 'os.urandom()' # Currently only 'random_id' needs 'os' to be imported, # for all those TLObjects with arg.can_be_inferred. From 519c113b5888cb61daea38db558cebf85068009f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 21:13:57 +0100 Subject: [PATCH 107/361] Update to v0.16.2 --- readthedocs/extra/changelog.rst | 46 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 9457c4f4..580ebe4b 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,52 @@ it can take advantage of new goodies! .. contents:: List of All Versions +New ``.resolve()`` method (v0.16.2) +=================================== + +*Published at 2018/01/19* + +The ``TLObject``'s (instances returned by the API and ``Request``'s) have +now acquired a new ``.resolve()`` method. While this should be used by the +library alone (when invoking a request), it means that you can now use +``Peer`` types or even usernames where a ``InputPeer`` is required. The +object now has access to the ``client``, so that it can fetch the right +type if needed, or access the session database. Furthermore, you can +reuse requests that need "autocast" (e.g. you put ``User`` but ``InputPeer`` +was needed), since ``.resolve()`` is called when invoking. Before, it was +only done on object construction. + +Additions +~~~~~~~~~ + +- Album support. Just pass a list, tuple or any iterable to ``.send_file()``. + + +Enhancements +~~~~~~~~~~~~ + +- ``.start()`` asks for your phone only if required. +- Better file cache. All files under 10MB, once uploaded, should never be + needed to be re-uploaded again, as the sent media is cached to the session. + + +Bug fixes +~~~~~~~~~ + +- ``setup.py`` now calls ``gen_tl`` when installing the library if needed. + + +Internal changes +~~~~~~~~~~~~~~~~ + +- The mentioned ``.resolve()`` to perform "autocast", more powerful. +- Upload and download methods are no longer part of ``TelegramBareClient``. +- Reuse ``.on_response()``, ``.__str__`` and ``.stringify()``. + Only override ``.on_response()`` if necessary (small amount of cases). +- Reduced "autocast" overhead as much as possible. + You shouldn't be penalized if you've provided the right type. + + MtProto 2.0 (v0.16.1) ===================== diff --git a/telethon/version.py b/telethon/version.py index e0220c01..28c39d24 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.16.1' +__version__ = '0.16.2' From 4d4e81e609dbb247281cc924196e7be2569a5d78 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 22:55:28 +0100 Subject: [PATCH 108/361] Fix cyclic imports on Python 3.4 by moving Session one level up --- telethon/{tl => }/session.py | 10 ++++------ telethon/telegram_bare_client.py | 3 ++- telethon/tl/__init__.py | 1 - 3 files changed, 6 insertions(+), 8 deletions(-) rename telethon/{tl => }/session.py (98%) diff --git a/telethon/tl/session.py b/telethon/session.py similarity index 98% rename from telethon/tl/session.py rename to telethon/session.py index bfed1a79..f7170478 100644 --- a/telethon/tl/session.py +++ b/telethon/session.py @@ -9,9 +9,10 @@ from enum import Enum from os.path import isfile as file_exists from threading import Lock -from .. import utils -from ..tl import TLObject -from ..tl.types import ( +from . import utils +from .crypto import AuthKey +from .tl import TLObject +from .tl.types import ( PeerUser, PeerChat, PeerChannel, InputPeerUser, InputPeerChat, InputPeerChannel, InputPhoto, InputDocument @@ -118,7 +119,6 @@ class Session: tuple_ = c.fetchone() if tuple_: self._dc_id, self._server_address, self._port, key, = tuple_ - from ..crypto import AuthKey self._auth_key = AuthKey(data=key) c.close() @@ -173,7 +173,6 @@ class Session: self._server_address = \ data.get('server_address', self._server_address) - from ..crypto import AuthKey if data.get('auth_key_data', None) is not None: key = b64decode(data['auth_key_data']) self._auth_key = AuthKey(data=key) @@ -228,7 +227,6 @@ class Session: c.execute('select auth_key from sessions') tuple_ = c.fetchone() if tuple_: - from ..crypto import AuthKey self._auth_key = AuthKey(data=tuple_[0]) else: self._auth_key = None diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index bee3ecdd..9b756f43 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -15,7 +15,8 @@ from .errors import ( PhoneMigrateError, NetworkMigrateError, UserMigrateError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode -from .tl import TLObject, Session +from .session import Session +from .tl import TLObject from .tl.all_tlobjects import LAYER from .tl.functions import ( InitConnectionRequest, InvokeWithLayerRequest, PingRequest diff --git a/telethon/tl/__init__.py b/telethon/tl/__init__.py index 403e481a..96c934bb 100644 --- a/telethon/tl/__init__.py +++ b/telethon/tl/__init__.py @@ -1,5 +1,4 @@ from .tlobject import TLObject -from .session import Session from .gzip_packed import GzipPacked from .tl_message import TLMessage from .message_container import MessageContainer From b716c4fe6792246c7bec3c06d8edd911b169f0ba Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 11:47:17 +0100 Subject: [PATCH 109/361] Several documentation enhancements and build warnings fixes - Made the documentation even more friendly towards newbies. - Eased the usage of methods like get history which now set a default empty message for message actions and vice versa. - Fixed some docstring documentations too. - Updated the old normal docs/ to link back and forth RTD. - Fixed the version of the documentation, now auto-loaded. --- docs/res/core.html | 78 +++++-------------- readthedocs/conf.py | 12 ++- .../advanced-usage/accessing-the-full-api.rst | 18 ++++- readthedocs/extra/basic/creating-a-client.rst | 2 + readthedocs/extra/basic/entities.rst | 53 ++++++++----- readthedocs/extra/basic/getting-started.rst | 35 +++++++-- readthedocs/extra/basic/installation.rst | 10 ++- readthedocs/extra/basic/telegram-client.rst | 31 ++++---- .../extra/basic/working-with-updates.rst | 6 ++ readthedocs/extra/examples/bots.rst | 5 ++ .../extra/examples/chats-and-channels.rst | 5 ++ .../extra/examples/working-with-messages.rst | 5 ++ readthedocs/index.rst | 15 ++-- readthedocs/telethon.rst | 13 +++- readthedocs/telethon.tl.rst | 16 ---- telethon/telegram_client.py | 6 +- telethon/tl/custom/dialog.py | 5 +- telethon/tl/custom/draft.py | 6 +- 18 files changed, 180 insertions(+), 141 deletions(-) diff --git a/docs/res/core.html b/docs/res/core.html index bc5c04b3..8c8bc9d8 100644 --- a/docs/res/core.html +++ b/docs/res/core.html @@ -44,8 +44,15 @@ page aims to provide easy access to all the available methods, their definition and parameters.

-

Although this documentation was generated for Telethon, it may - be useful for any other Telegram library out there.

+

Please note that when you see this:

+
---functions---
+users.getUsers#0d91a548 id:Vector<InputUser> = Vector<User>
+ +

This is not Python code. It's the "TL definition". It's + an easy-to-read line that gives a quick overview on the parameters + and its result. You don't need to worry about this. See + here + for more details on it.

Index

    @@ -69,12 +76,12 @@

    Currently there are {method_count} methods available for the layer {layer}. The complete list can be seen here.

    - Methods, also known as requests, are used to interact with - the Telegram API itself and are invoked with a call to .invoke(). - Only these can be passed to .invoke()! You cannot - .invoke() types or constructors, only requests. After this, - Telegram will return a result, which may be, for instance, - a bunch of messages, some dialogs, users, etc.

    + Methods, also known as requests, are used to interact with the + Telegram API itself and are invoked through client(Request(...)). + Only these can be used like that! You cannot invoke types or + constructors, only requests. After this, Telegram will return a + result, which may be, for instance, a bunch of messages, + some dialogs, users, etc.

    Types

    Currently there are {type_count} types. You can see the full @@ -151,58 +158,9 @@

Full example

-

The following example demonstrates:

-
    -
  1. How to create a TelegramClient.
  2. -
  3. Connecting to the Telegram servers and authorizing an user.
  4. -
  5. Retrieving a list of chats (dialogs).
  6. -
  7. Invoking a request without the built-in methods.
  8. -
-
#!/usr/bin/python3
-from telethon import TelegramClient
-from telethon.tl.functions.messages import GetHistoryRequest
-
-# (1) Use your own values here
-api_id   = 12345
-api_hash = '0123456789abcdef0123456789abcdef'
-phone    = '+34600000000'
-
-# (2) Create the client and connect
-client = TelegramClient('username', api_id, api_hash)
-client.connect()
-
-# Ensure you're authorized
-if not client.is_user_authorized():
-    client.send_code_request(phone)
-    client.sign_in(phone, input('Enter the code: '))
-
-# (3) Using built-in methods
-dialogs, entities = client.get_dialogs(10)
-entity = entities[0]
-
-# (4) !! Invoking a request manually !!
-result = client(GetHistoryRequest(
-    entity,
-    limit=20,
-    offset_date=None,
-    offset_id=0,
-    max_id=0,
-    min_id=0,
-    add_offset=0
-))
-
-# Now you have access to the first 20 messages
-messages = result.messages
- -

As it can be seen, manually calling requests with - client(request) (or using the old way, by calling - client.invoke(request)) is way more verbose than using the - built-in methods (such as client.get_dialogs()).

- -

However, and - given that there are so many methods available, it's impossible to provide - a nice interface to things that may change over time. To get full access, - however, you're still able to invoke these methods manually.

+

Documentation for this is now + here. +

diff --git a/readthedocs/conf.py b/readthedocs/conf.py index 18ff1a17..efb14992 100644 --- a/readthedocs/conf.py +++ b/readthedocs/conf.py @@ -20,6 +20,11 @@ # import os # import sys # sys.path.insert(0, os.path.abspath('.')) +import os +import re + + +root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir)) # -- General configuration ------------------------------------------------ @@ -55,9 +60,12 @@ author = 'Lonami' # built documents. # # The short X.Y version. -version = '0.15' +with open(os.path.join(root, 'telethon', 'version.py')) as f: + version = re.search(r"^__version__\s+=\s+'(.*)'$", + f.read(), flags=re.MULTILINE).group(1) + # The full version, including alpha/beta/rc tags. -release = '0.15.5' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index 04659bdb..7276aa43 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -14,8 +14,10 @@ through a sorted list of everything you can do. .. note:: - Removing the hand crafted documentation for methods is still - a work in progress! + The reason to keep both https://lonamiwebs.github.io/Telethon and this + documentation alive is that the former allows instant search results + as you type, and a "Copy import" button. If you like namespaces, you + can also do ``from telethon.tl import types, functions``. Both work. You should also refer to the documentation to see what the objects @@ -39,8 +41,8 @@ If you're going to use a lot of these, you may do: .. code-block:: python - import telethon.tl.functions as tl - # We now have access to 'tl.messages.SendMessageRequest' + from telethon.tl import types, functions + # We now have access to 'functions.messages.SendMessageRequest' We see that this request must take at least two parameters, a ``peer`` of type `InputPeer`__, and a ``message`` which is just a Python @@ -82,6 +84,14 @@ every time its used, simply call ``.get_input_peer``: from telethon import utils peer = utils.get_input_user(entity) + +.. note:: + + Since ``v0.16.2`` this is further simplified. The ``Request`` itself + will call ``client.get_input_entity()`` for you when required, but + it's good to remember what's happening. + + After this small parenthesis about ``.get_entity`` versus ``.get_input_entity``, we have everything we need. To ``.invoke()`` our request we do: diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 10ae5f60..bf565bb0 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -93,6 +93,8 @@ method also accepts a ``phone=`` and ``bot_token`` parameters. You can use either, as both will work. Determining which is just a matter of taste, and how much control you need. +Remember that you can get yourself at any time with ``client.get_me()``. + .. note:: If you want to use a **proxy**, you have to `install PySocks`__ diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index bc87539a..472942a7 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -10,21 +10,6 @@ The library widely uses the concept of "entities". An entity will refer to any ``User``, ``Chat`` or ``Channel`` object that the API may return in response to certain methods, such as ``GetUsersRequest``. -To save bandwidth, the API also makes use of their "input" versions. -The input version of an entity (e.g. ``InputPeerUser``, ``InputChat``, -etc.) only contains the minimum required information that's required -for Telegram to be able to identify who you're referring to: their ID -and hash. This ID/hash pair is unique per user, so if you use the pair -given by another user **or bot** it will **not** work. - -To save *even more* bandwidth, the API also makes use of the ``Peer`` -versions, which just have an ID. This serves to identify them, but -peers alone are not enough to use them. You need to know their hash -before you can "use them". - -Luckily, the library tries to simplify this mess the best it can. - - Getting entities **************** @@ -58,8 +43,8 @@ you're able to just do this: my_channel = client.get_entity(PeerChannel(some_id)) -All methods in the :ref:`telegram-client` call ``.get_entity()`` to further -save you from the hassle of doing so manually, so doing things like +All methods in the :ref:`telegram-client` call ``.get_input_entity()`` to +further save you from the hassle of doing so manually, so doing things like ``client.send_message('lonami', 'hi!')`` is possible. Every entity the library "sees" (in any response to any call) will by @@ -72,7 +57,27 @@ made to obtain the required information. Entities vs. Input Entities *************************** -As we mentioned before, API calls don't need to know the whole information +.. note:: + + Don't worry if you don't understand this section, just remember some + of the details listed here are important. When you're calling a method, + don't call ``.get_entity()`` before, just use the username or phone, + or the entity retrieved by other means like ``.get_dialogs()``. + + +To save bandwidth, the API also makes use of their "input" versions. +The input version of an entity (e.g. ``InputPeerUser``, ``InputChat``, +etc.) only contains the minimum required information that's required +for Telegram to be able to identify who you're referring to: their ID +and hash. This ID/hash pair is unique per user, so if you use the pair +given by another user **or bot** it will **not** work. + +To save *even more* bandwidth, the API also makes use of the ``Peer`` +versions, which just have an ID. This serves to identify them, but +peers alone are not enough to use them. You need to know their hash +before you can "use them". + +As we just mentioned, API calls don't need to know the whole information about the entities, only their ID and hash. For this reason, another method, ``.get_input_entity()`` is available. This will always use the cache while possible, making zero API calls most of the time. When a request is made, @@ -85,3 +90,15 @@ the most recent information about said entity, but invoking requests don't need this information, just the ``InputPeer``. Only use ``.get_entity()`` if you need to get actual information, like the username, name, title, etc. of the entity. + +To further simplify the workflow, since the version ``0.16.2`` of the +library, the raw requests you make to the API are also able to call +``.get_input_entity`` wherever needed, so you can even do things like: + + .. code-block:: python + + client(SendMessageRequest('username', 'hello')) + +The library will call the ``.resolve()`` method of the request, which will +resolve ``'username'`` with the appropriated ``InputPeer``. Don't worry if +you don't get this yet, but remember some of the details here are important. diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index e69cc3ef..87c142e9 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -1,7 +1,5 @@ -.. Telethon documentation master file, created by - sphinx-quickstart on Fri Nov 17 15:36:11 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +.. _getting-started: + =============== Getting Started @@ -39,13 +37,36 @@ Basic Usage .. code-block:: python - print(me.stringify()) + # Getting information about yourself + print(client.get_me().stringify()) - client.send_message('username', 'Hello! Talking to you from Telethon') + # Sending a message (you can use 'me' or 'self' to message yourself) + client.send_message('username', 'Hello World from Telethon!') + + # Sending a file client.send_file('username', '/home/myself/Pictures/holidays.jpg') - client.download_profile_photo(me) + # Retrieving messages from a chat + from telethon import utils + for message in client.get_message_history('username', limit=10): + print(utils.get_display_name(message.sender), message.message) + + # Listing all the dialogs (conversations you have open) + for dialog in client.get_dialogs(limit=10): + print(utils.get_display_name(dialog.entity), dialog.draft.message) + + # Downloading profile photos (default path is the working directory) + client.download_profile_photo('username') + + # Once you have a message with .media (if message.media) + # you can download it using client.download_media(): messages = client.get_message_history('username') client.download_media(messages[0]) **More details**: :ref:`telegram-client` + + +---------- + +You can continue by clicking on the "More details" link below each +snippet of code or the "Next" button at the bottom of the page. diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index 945576d0..e74cdae6 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -29,7 +29,9 @@ You can also install the library directly from GitHub or a fork: $ cd Telethon/ # pip install -Ue . -If you don't have root access, simply pass the ``--user`` flag to the pip command. +If you don't have root access, simply pass the ``--user`` flag to the pip +command. If you want to install a specific branch, append ``@branch`` to +the end of the first install command. Manual Installation @@ -49,7 +51,8 @@ Manual Installation 5. Done! -To generate the documentation, ``cd docs`` and then ``python3 generate.py``. +To generate the `method documentation`__, ``cd docs`` and then +``python3 generate.py`` (if some pages render bad do it twice). Optional dependencies @@ -62,5 +65,6 @@ will also work without it. __ https://github.com/ricmoo/pyaes __ https://pypi.python.org/pypi/pyaes -__ https://github.com/sybrenstuvel/python-rsa/ +__ https://github.com/sybrenstuvel/python-rsa __ https://pypi.python.org/pypi/rsa/3.4.2 +__ https://lonamiwebs.github.io/Telethon diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst index 5663f533..d3375200 100644 --- a/readthedocs/extra/basic/telegram-client.rst +++ b/readthedocs/extra/basic/telegram-client.rst @@ -43,30 +43,29 @@ how the library refers to either of these: lonami = client.get_entity('lonami') The so called "entities" are another important whole concept on its own, -and you should -Note that saving and using these entities will be more important when -Accessing the Full API. For now, this is a good way to get information -about an user or chat. +but for now you don't need to worry about it. Simply know that they are +a good way to get information about an user, chat or channel. -Other common methods for quick scripts are also available: +Many other common methods for quick scripts are also available: .. code-block:: python - # Sending a message (use an entity/username/etc) - client.send_message('TheAyyBot', 'ayy') + # Note that you can use 'me' or 'self' to message yourself + client.send_message('username', 'Hello World from Telethon!') - # Sending a photo, or a file - client.send_file(myself, '/path/to/the/file.jpg', force_document=True) + client.send_file('username', '/home/myself/Pictures/holidays.jpg') - # Downloading someone's profile photo. File is saved to 'where' - where = client.download_profile_photo(someone) + # The utils package has some goodies, like .get_display_name() + from telethon import utils + for message in client.get_message_history('username', limit=10): + print(utils.get_display_name(message.sender), message.message) - # Retrieving the message history - messages = client.get_message_history(someone) + # Dialogs are the conversations you have open + for dialog in client.get_dialogs(limit=10): + print(utils.get_display_name(dialog.entity), dialog.draft.message) - # Downloading the media from a specific message - # You can specify either a directory, a filename, or nothing at all - where = client.download_media(message, '/path/to/output') + # Default path is the working directory + client.download_profile_photo('username') # Call .disconnect() when you're done client.disconnect() diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index bb78eb97..72155d86 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -4,6 +4,12 @@ Working with Updates ==================== + +.. note:: + + There are plans to make working with updates more friendly. Stay tuned! + + .. contents:: diff --git a/readthedocs/extra/examples/bots.rst b/readthedocs/extra/examples/bots.rst index b231e200..fd4d54de 100644 --- a/readthedocs/extra/examples/bots.rst +++ b/readthedocs/extra/examples/bots.rst @@ -3,6 +3,11 @@ Bots ==== +.. note:: + + These examples assume you have read :ref:`accessing-the-full-api`. + + Talking to Inline Bots ********************** diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 99ce235f..be836b16 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -3,6 +3,11 @@ Working with Chats and Channels =============================== +.. note:: + + These examples assume you have read :ref:`accessing-the-full-api`. + + Joining a chat or channel ************************* diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index 880bac6f..43492605 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -3,6 +3,11 @@ Working with messages ===================== +.. note:: + + These examples assume you have read :ref:`accessing-the-full-api`. + + Forwarding messages ******************* diff --git a/readthedocs/index.rst b/readthedocs/index.rst index cae75541..74c3b8e6 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -10,8 +10,12 @@ Welcome to Telethon's documentation! Pure Python 3 Telegram client library. Official Site `here `_. -Please follow the links below to get you started, and remember -to read the :ref:`changelog` when you upgrade! +Please follow the links on the index below to navigate from here, +or use the menu on the left. Remember to read the :ref:`changelog` +when you upgrade! + +.. important:: + If you're new here, you want to read :ref:`getting-started`. What is this? @@ -85,19 +89,20 @@ heavy job for you, so you can focus on developing an application. extra/developing/telegram-api-in-other-languages.rst -.. _Wall-of-shame: +.. _More: .. toctree:: :maxdepth: 2 - :caption: Wall of Shame + :caption: More + extra/changelog extra/wall-of-shame.rst .. toctree:: :caption: Telethon modules - telethon + modules Indices and tables diff --git a/readthedocs/telethon.rst b/readthedocs/telethon.rst index 2d3c269c..e7a30c42 100644 --- a/readthedocs/telethon.rst +++ b/readthedocs/telethon.rst @@ -42,6 +42,13 @@ telethon\.utils module :undoc-members: :show-inheritance: +telethon\.session module +------------------------ + +.. automodule:: telethon.session + :members: + :undoc-members: + :show-inheritance: telethon\.cryto package ------------------------ @@ -58,21 +65,21 @@ telethon\.errors package telethon.errors telethon\.extensions package ------------------------- +---------------------------- .. toctree:: telethon.extensions telethon\.network package ------------------------- +------------------------- .. toctree:: telethon.network telethon\.tl package ------------------------- +-------------------- .. toctree:: diff --git a/readthedocs/telethon.tl.rst b/readthedocs/telethon.tl.rst index 6fbb1f00..a10ecc68 100644 --- a/readthedocs/telethon.tl.rst +++ b/readthedocs/telethon.tl.rst @@ -7,14 +7,6 @@ telethon\.tl package telethon.tl.custom -telethon\.tl\.entity\_database module -------------------------------------- - -.. automodule:: telethon.tl.entity_database - :members: - :undoc-members: - :show-inheritance: - telethon\.tl\.gzip\_packed module --------------------------------- @@ -31,14 +23,6 @@ telethon\.tl\.message\_container module :undoc-members: :show-inheritance: -telethon\.tl\.session module ----------------------------- - -.. automodule:: telethon.tl.session - :members: - :undoc-members: - :show-inheritance: - telethon\.tl\.tl\_message module -------------------------------- diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f09a62fa..5fe186f3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -406,7 +406,7 @@ class TelegramClient(TelegramBareClient): def log_out(self): """ - Logs out Telegram and deletes the current *.session file. + Logs out Telegram and deletes the current ``*.session`` file. Returns: True if the operation was successful. @@ -742,6 +742,10 @@ class TelegramClient(TelegramBareClient): # Add a few extra attributes to the Message to make it friendlier. messages.total = total_messages for m in messages: + # To make messages more friendly, always add message + # to service messages, and action to normal messages. + m.message = getattr(m, 'message', None) + m.action = getattr(m, 'action', None) m.sender = (None if not m.from_id else entities[utils.get_peer_id(m.from_id)]) diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index fd36ba8f..366a19bf 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -24,10 +24,7 @@ class Dialog: self.unread_count = dialog.unread_count self.unread_mentions_count = dialog.unread_mentions_count - if dialog.draft: - self.draft = Draft(client, dialog.peer, dialog.draft) - else: - self.draft = None + self.draft = Draft(client, dialog.peer, dialog.draft) def send_message(self, *args, **kwargs): """ diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index abf84548..ae08403a 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -1,16 +1,18 @@ from ..functions.messages import SaveDraftRequest -from ..types import UpdateDraftMessage +from ..types import UpdateDraftMessage, DraftMessage 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()`. + instances of this class when calling ``client.get_drafts()``. """ def __init__(self, client, peer, draft): self._client = client self._peer = peer + if not draft: + draft = DraftMessage('', None, None, None, None) self.text = draft.message self.date = draft.date From 3379330f9b2872b8e7ca56f3e70872ec3986a39a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 12:25:31 +0100 Subject: [PATCH 110/361] Add an exact match list on the documentation --- docs/res/core.html | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/res/core.html b/docs/res/core.html index 8c8bc9d8..368a04d5 100644 --- a/docs/res/core.html +++ b/docs/res/core.html @@ -19,6 +19,11 @@ placeholder="Search for requests and types…" />
+
Methods (0)
    @@ -179,6 +184,10 @@ typesCount = document.getElementById("typesCount"); constructorsList = document.getElementById("constructorsList"); constructorsCount = document.getElementById("constructorsCount"); +// Exact match +exactMatch = document.getElementById("exactMatch"); +exactList = document.getElementById("exactList"); + try { requests = [{request_names}]; types = [{type_names}]; @@ -225,7 +234,9 @@ function buildList(countSpan, resultList, foundElements) { result += ''; } - countSpan.innerHTML = "" + foundElements[0].length; + if (countSpan) { + countSpan.innerHTML = "" + foundElements[0].length; + } resultList.innerHTML = result; } @@ -245,6 +256,26 @@ function updateSearch() { buildList(methodsCount, methodsList, foundRequests); buildList(typesCount, typesList, foundTypes); buildList(constructorsCount, constructorsList, foundConstructors); + + // Now look for exact matches + var original = requests.concat(constructors); + var originalu = requestsu.concat(constructorsu); + var destination = []; + var destinationu = []; + + for (var i = 0; i < original.length; ++i) { + if (original[i].toLowerCase().replace("request", "") == query) { + destination.push(original[i]); + destinationu.push(originalu[i]); + } + } + + if (destination.length == 0) { + exactMatch.style.display = "none"; + } else { + exactMatch.style.display = ""; + buildList(null, exactList, [destination, destinationu]); + } } else { contentDiv.style.display = ""; searchDiv.style.display = "none"; From 644105d0384634918ac6137e9614d1f2f88ee431 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 13:11:22 +0100 Subject: [PATCH 111/361] Separate docs search into its own script and use it everywhere --- docs/docs_writer.py | 11 ++- docs/generate.py | 56 ++++++++------ docs/res/core.html | 162 +-------------------------------------- docs/res/js/search.js | 172 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 184 deletions(-) create mode 100644 docs/res/js/search.js diff --git a/docs/docs_writer.py b/docs/docs_writer.py index 9eec6cd7..82241a48 100644 --- a/docs/docs_writer.py +++ b/docs/docs_writer.py @@ -28,6 +28,7 @@ class DocsWriter: self.table_columns = 0 self.table_columns_left = None self.write_copy_script = False + self._script = '' # High level writing def write_head(self, title, relative_css_path): @@ -254,6 +255,12 @@ class DocsWriter: self.write('' .format(text_to_copy, text)) + def add_script(self, src='', relative_src=None): + if relative_src: + self._script += ''.format(relative_src) + elif src: + self._script += ''.format(src) + def end_body(self): """Ends the whole document. This should be called the last""" if self.write_copy_script: @@ -268,7 +275,9 @@ class DocsWriter: 'catch(e){}}' '') - self.write('
') + self.write('') + self.write(self._script) + self.write('') # "Low" level writing def write(self, s): diff --git a/docs/generate.py b/docs/generate.py index 4feb1518..ae2bd43c 100755 --- a/docs/generate.py +++ b/docs/generate.py @@ -224,6 +224,16 @@ def get_description(arg): return ' '.join(desc) +def copy_replace(src, dst, replacements): + """Copies the src file into dst applying the replacements dict""" + with open(src) as infile, open(dst, 'w') as outfile: + outfile.write(re.sub( + '|'.join(re.escape(k) for k in replacements), + lambda m: str(replacements[m.group(0)]), + infile.read() + )) + + def generate_documentation(scheme_file): """Generates the documentation HTML files from from scheme.tl to /methods and /constructors, etc. @@ -231,6 +241,7 @@ def generate_documentation(scheme_file): original_paths = { 'css': 'css/docs.css', 'arrow': 'img/arrow.svg', + 'search.js': 'js/search.js', '404': '404.html', 'index_all': 'index.html', 'index_types': 'types/index.html', @@ -366,6 +377,10 @@ def generate_documentation(scheme_file): else: docs.write_text('This type has no members.') + # TODO Bit hacky, make everything like this? (prepending '../') + depth = '../' * (2 if tlobject.namespace else 1) + docs.add_script(src='prependPath = "{}";'.format(depth)) + docs.add_script(relative_src=paths['search.js']) docs.end_body() # Find all the available types (which are not the same as the constructors) @@ -540,36 +555,31 @@ def generate_documentation(scheme_file): type_urls = fmt(types, get_path_for_type) constructor_urls = fmt(constructors, get_create_path_for) - replace_dict = { - 'type_count': len(types), - 'method_count': len(methods), - 'constructor_count': len(tlobjects) - len(methods), - 'layer': layer, - - 'request_names': request_names, - 'type_names': type_names, - 'constructor_names': constructor_names, - 'request_urls': request_urls, - 'type_urls': type_urls, - 'constructor_urls': constructor_urls - } - shutil.copy('../res/404.html', original_paths['404']) - - with open('../res/core.html') as infile,\ - open(original_paths['index_all'], 'w') as outfile: - text = infile.read() - for key, value in replace_dict.items(): - text = text.replace('{' + key + '}', str(value)) - - outfile.write(text) + copy_replace('../res/core.html', original_paths['index_all'], { + '{type_count}': len(types), + '{method_count}': len(methods), + '{constructor_count}': len(tlobjects) - len(methods), + '{layer}': layer, + }) + os.makedirs(os.path.abspath(os.path.join( + original_paths['search.js'], os.path.pardir + )), exist_ok=True) + copy_replace('../res/js/search.js', original_paths['search.js'], { + '{request_names}': request_names, + '{type_names}': type_names, + '{constructor_names}': constructor_names, + '{request_urls}': request_urls, + '{type_urls}': type_urls, + '{constructor_urls}': constructor_urls + }) # Everything done print('Documentation generated.') def copy_resources(): - for d in ['css', 'img']: + for d in ('css', 'img'): os.makedirs(d, exist_ok=True) shutil.copy('../res/img/arrow.svg', 'img') diff --git a/docs/res/core.html b/docs/res/core.html index 368a04d5..0d1673aa 100644 --- a/docs/res/core.html +++ b/docs/res/core.html @@ -14,34 +14,7 @@
- - - -
- - -
Methods (0) -
    -
-
- -
Types (0) -
    -
-
- -
Constructors (0) -
    -
-
-
- -
+

Telethon API

This documentation was generated straight from the scheme.tl provided by Telegram. However, there is no official documentation per se @@ -167,137 +140,6 @@ users.getUsers#0d91a548 id:Vector<InputUser> = Vector<User> here.

- -
- + diff --git a/docs/res/js/search.js b/docs/res/js/search.js new file mode 100644 index 00000000..c63672e7 --- /dev/null +++ b/docs/res/js/search.js @@ -0,0 +1,172 @@ +root = document.getElementById("main_div"); +root.innerHTML = ` + + + +
+ + +
Methods (0) +
    +
+
+ +
Types (0) +
    +
+
+ +
Constructors (0) +
    +
+
+
+
+` + root.innerHTML + "
"; + +// HTML modified, now load documents +contentDiv = document.getElementById("contentDiv"); +searchDiv = document.getElementById("searchDiv"); +searchBox = document.getElementById("searchBox"); + +// Search lists +methodsList = document.getElementById("methodsList"); +methodsCount = document.getElementById("methodsCount"); + +typesList = document.getElementById("typesList"); +typesCount = document.getElementById("typesCount"); + +constructorsList = document.getElementById("constructorsList"); +constructorsCount = document.getElementById("constructorsCount"); + +// Exact match +exactMatch = document.getElementById("exactMatch"); +exactList = document.getElementById("exactList"); + +try { + requests = [{request_names}]; + types = [{type_names}]; + constructors = [{constructor_names}]; + + requestsu = [{request_urls}]; + typesu = [{type_urls}]; + constructorsu = [{constructor_urls}]; +} catch (e) { + requests = []; + types = []; + constructors = []; + requestsu = []; + typesu = []; + constructorsu = []; +} + +if (typeof prependPath !== 'undefined') { + for (var i = 0; i != requestsu.length; ++i) { + requestsu[i] = prependPath + requestsu[i]; + } + for (var i = 0; i != typesu.length; ++i) { + typesu[i] = prependPath + typesu[i]; + } + for (var i = 0; i != constructorsu.length; ++i) { + constructorsu[i] = prependPath + constructorsu[i]; + } +} + +// Given two input arrays "original" and "original urls" and a query, +// return a pair of arrays with matching "query" elements from "original". +// +// TODO Perhaps return an array of pairs instead a pair of arrays (for cache). +function getSearchArray(original, originalu, query) { + var destination = []; + var destinationu = []; + + for (var i = 0; i < original.length; ++i) { + if (original[i].toLowerCase().indexOf(query) != -1) { + destination.push(original[i]); + destinationu.push(originalu[i]); + } + } + + return [destination, destinationu]; +} + +// Modify "countSpan" and "resultList" accordingly based on the elements +// given as [[elements], [element urls]] (both with the same length) +function buildList(countSpan, resultList, foundElements) { + var result = ""; + for (var i = 0; i < foundElements[0].length; ++i) { + result += '
  • '; + result += ''; + result += foundElements[0][i]; + result += '
  • '; + } + + if (countSpan) { + countSpan.innerHTML = "" + foundElements[0].length; + } + resultList.innerHTML = result; +} + +function updateSearch() { + if (searchBox.value) { + contentDiv.style.display = "none"; + searchDiv.style.display = ""; + + var query = searchBox.value.toLowerCase(); + + var foundRequests = getSearchArray(requests, requestsu, query); + var foundTypes = getSearchArray(types, typesu, query); + var foundConstructors = getSearchArray( + constructors, constructorsu, query + ); + + buildList(methodsCount, methodsList, foundRequests); + buildList(typesCount, typesList, foundTypes); + buildList(constructorsCount, constructorsList, foundConstructors); + + // Now look for exact matches + var original = requests.concat(constructors); + var originalu = requestsu.concat(constructorsu); + var destination = []; + var destinationu = []; + + for (var i = 0; i < original.length; ++i) { + if (original[i].toLowerCase().replace("request", "") == query) { + destination.push(original[i]); + destinationu.push(originalu[i]); + } + } + + if (destination.length == 0) { + exactMatch.style.display = "none"; + } else { + exactMatch.style.display = ""; + buildList(null, exactList, [destination, destinationu]); + } + } else { + contentDiv.style.display = ""; + searchDiv.style.display = "none"; + } +} + +function getQuery(name) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i != vars.length; ++i) { + var pair = vars[i].split("="); + if (pair[0] == name) + return pair[1]; + } +} + +var query = getQuery('q'); +if (query) { + searchBox.value = query; +} + +updateSearch(); From 86816a3bdf7281866b89567f427810165e42c3b1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 19:29:05 +0100 Subject: [PATCH 112/361] Add missing InputChannel case on .get_input_peer() --- telethon/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index a4850d4c..d4be0e25 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -97,15 +97,18 @@ def get_input_peer(entity, allow_self=True): return InputPeerChannel(entity.id, entity.access_hash or 0) # Less common cases - if isinstance(entity, UserEmpty): - return InputPeerEmpty() - if isinstance(entity, InputUser): return InputPeerUser(entity.user_id, entity.access_hash) + if isinstance(entity, InputChannel): + return InputPeerChannel(entity.channel_id, entity.access_hash) + if isinstance(entity, InputUserSelf): return InputPeerSelf() + if isinstance(entity, UserEmpty): + return InputPeerEmpty() + if isinstance(entity, UserFull): return get_input_peer(entity.user) From f1371c3999c8936434f08c0ff774d3e9fb9e7528 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 19:39:48 +0100 Subject: [PATCH 113/361] Early return from Session.get_input_entity() if Input* given --- telethon/session.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/telethon/session.py b/telethon/session.py index f7170478..21c0e105 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -401,12 +401,16 @@ class Session: Raises ValueError if it cannot be found. """ - if isinstance(key, TLObject): - try: - # Try to early return if this key can be casted as input peer - return utils.get_input_peer(key) - except TypeError: - # Otherwise, get the ID of the peer + try: + if key.SUBCLASS_OF_ID in (0xc91c90b6, 0xe669bf46, 0x40f202fd): + # hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel')) + # We already have an Input version, so nothing else required + return key + # Try to early return if this key can be casted as input peer + return utils.get_input_peer(key) + except (AttributeError, TypeError): + # Not a TLObject or can't be cast into InputPeer + if isinstance(key, TLObject): key = utils.get_peer_id(key) c = self._conn.cursor() From ec38bd94d8036f08ad99e2da901b94696f48ac1d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 19:50:48 +0100 Subject: [PATCH 114/361] Fix .rst not showing code blocks on "unknown" languages --- .../extra/developing/understanding-the-type-language.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/readthedocs/extra/developing/understanding-the-type-language.rst b/readthedocs/extra/developing/understanding-the-type-language.rst index c82063ef..8e5259a7 100644 --- a/readthedocs/extra/developing/understanding-the-type-language.rst +++ b/readthedocs/extra/developing/understanding-the-type-language.rst @@ -10,9 +10,7 @@ what other programming languages commonly call classes or structs. Every definition is written as follows for a Telegram object is defined as follows: -.. code:: tl - - name#id argument_name:argument_type = CommonType + ``name#id argument_name:argument_type = CommonType`` This means that in a single line you know what the ``TLObject`` name is. You know it's unique ID, and you know what arguments it has. It really From 182b6fc1cb3ea1dd7ee9d29522bd0531344a81d0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 21 Jan 2018 10:57:58 +0100 Subject: [PATCH 115/361] Update old examples --- .../extra/examples/chats-and-channels.rst | 7 ++++--- .../extra/examples/working-with-messages.rst | 20 ++++++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index be836b16..30b94178 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -66,7 +66,7 @@ which use is very straightforward: client(AddChatUserRequest( chat_id, user_to_add, - fwd_limit=10 # allow the user to see the 10 last messages + fwd_limit=10 # Allow the user to see the 10 last messages )) @@ -111,8 +111,9 @@ a fixed limit: all_participants = [] while True: - participants = client.invoke(GetParticipantsRequest( - channel, ChannelParticipantsSearch(''), offset, limit + participants = client(GetParticipantsRequest( + channel, ChannelParticipantsSearch(''), offset, limit, + hash=0 )) if not participants.users: break diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index 43492605..ab38788c 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -47,12 +47,26 @@ into issues_. A valid example would be: .. code-block:: python + from telethon.tl.functions.messages import SearchRequest + from telethon.tl.types import InputMessagesFilterEmpty + + filter = InputMessagesFilterEmpty() result = client(SearchRequest( - entity, 'query', InputMessagesFilterEmpty(), None, None, 0, 0, 100 + peer=peer, # On which chat/conversation + q='query', # What to search for + filter=filter, # Filter to use (maybe filter for media) + min_date=None, # Minimum date + max_date=None, # Maximum date + offset_id=0, # ID of the message to use as offset + add_offset=0, # Additional offset + limit=10, # How many results + max_id=0, # Maximum message ID + min_id=0, # Minimum message ID + from_id=None # Who must have sent the message (peer) )) -It's important to note that the optional parameter ``from_id`` has been left -omitted and thus defaults to ``None``. Changing it to InputUserEmpty_, as one +It's important to note that the optional parameter ``from_id`` could have +been omitted (defaulting to ``None``). Changing it to InputUserEmpty_, as one could think to specify "no user", won't work because this parameter is a flag, and it being unspecified has a different meaning. From abe26625e6a733e1b6f73ecf5798539aacbaa1cf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 21 Jan 2018 11:04:46 +0100 Subject: [PATCH 116/361] Add missing ResolvedPeer, InputNotifyPeer, TopPeer cases --- telethon/utils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index d4be0e25..d113da73 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -6,6 +6,7 @@ import math import re from mimetypes import add_type, guess_extension +from .tl.types.contacts import ResolvedPeer from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty, @@ -20,7 +21,8 @@ from .tl.types import ( GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty, InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull, - InputMediaUploadedPhoto, DocumentAttributeFilename, photos + InputMediaUploadedPhoto, DocumentAttributeFilename, photos, + TopPeer, InputNotifyPeer ) USERNAME_RE = re.compile( @@ -362,8 +364,11 @@ def get_peer_id(peer): try: if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): - # Not a Peer or an InputPeer, so first get its Input version - peer = get_input_peer(peer, allow_self=False) + if isinstance(peer, (ResolvedPeer, InputNotifyPeer, TopPeer)): + peer = peer.peer + else: + # Not a Peer or an InputPeer, so first get its Input version + peer = get_input_peer(peer, allow_self=False) except AttributeError: _raise_cast_fail(peer, 'int') From 5f2f04c6c2676201cbb19e894d7d5bb3f3335ff0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 22 Jan 2018 11:06:11 +0200 Subject: [PATCH 117/361] Add HTML parse mode (#554) --- telethon/extensions/html.py | 167 ++++++++++++++++++++++++++++++++++++ telethon/telegram_client.py | 4 +- 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 telethon/extensions/html.py diff --git a/telethon/extensions/html.py b/telethon/extensions/html.py new file mode 100644 index 00000000..8cd170cb --- /dev/null +++ b/telethon/extensions/html.py @@ -0,0 +1,167 @@ +""" +Simple HTML -> Telegram entity parser. +""" +from html import escape, unescape +from html.parser import HTMLParser +from collections import deque + +from ..tl.types import ( + MessageEntityBold, MessageEntityItalic, MessageEntityCode, + MessageEntityPre, MessageEntityEmail, MessageEntityUrl, + MessageEntityTextUrl +) + + +class HTMLToTelegramParser(HTMLParser): + def __init__(self): + super().__init__() + self.text = '' + self.entities = [] + self._building_entities = {} + self._open_tags = deque() + self._open_tags_meta = deque() + + def handle_starttag(self, tag, attrs): + self._open_tags.appendleft(tag) + self._open_tags_meta.appendleft(None) + + attrs = dict(attrs) + EntityType = None + args = {} + if tag == 'strong' or tag == 'b': + EntityType = MessageEntityBold + elif tag == 'em' or tag == 'i': + EntityType = MessageEntityItalic + elif tag == 'code': + try: + # If we're in the middle of a
     tag, this  tag is
    +                # probably intended for syntax highlighting.
    +                #
    +                # Syntax highlighting is set with
    +                #     codeblock
    +                # inside 
     tags
    +                pre = self._building_entities['pre']
    +                try:
    +                    pre.language = attrs['class'][len('language-'):]
    +                except KeyError:
    +                    pass
    +            except KeyError:
    +                EntityType = MessageEntityCode
    +        elif tag == 'pre':
    +            EntityType = MessageEntityPre
    +            args['language'] = ''
    +        elif tag == 'a':
    +            try:
    +                url = attrs['href']
    +            except KeyError:
    +                return
    +            if url.startswith('mailto:'):
    +                url = url[len('mailto:'):]
    +                EntityType = MessageEntityEmail
    +            else:
    +                if self.get_starttag_text() == url:
    +                    EntityType = MessageEntityUrl
    +                else:
    +                    EntityType = MessageEntityTextUrl
    +                    args['url'] = url
    +                    url = None
    +            self._open_tags_meta.popleft()
    +            self._open_tags_meta.appendleft(url)
    +
    +        if EntityType and tag not in self._building_entities:
    +            self._building_entities[tag] = EntityType(
    +                offset=len(self.text),
    +                # The length will be determined when closing the tag.
    +                length=0,
    +                **args)
    +
    +    def handle_data(self, text):
    +        text = unescape(text)
    +
    +        previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
    +        if previous_tag == 'a':
    +            url = self._open_tags_meta[0]
    +            if url:
    +                text = url
    +
    +        for tag, entity in self._building_entities.items():
    +            entity.length += len(text.strip('\n'))
    +
    +        self.text += text
    +
    +    def handle_endtag(self, tag):
    +        try:
    +            self._open_tags.popleft()
    +            self._open_tags_meta.popleft()
    +        except IndexError:
    +            pass
    +        entity = self._building_entities.pop(tag, None)
    +        if entity:
    +            self.entities.append(entity)
    +
    +
    +def parse(html):
    +    """
    +    Parses the given HTML message and returns its stripped representation
    +    plus a list of the MessageEntity's that were found.
    +
    +    :param message: the message with HTML to be parsed.
    +    :return: a tuple consisting of (clean message, [message entities]).
    +    """
    +    parser = HTMLToTelegramParser()
    +    parser.feed(html)
    +    return parser.text, parser.entities
    +
    +
    +def unparse(text, entities):
    +    """
    +    Performs the reverse operation to .parse(), effectively returning HTML
    +    given a normal text and its MessageEntity's.
    +
    +    :param text: the text to be reconverted into HTML.
    +    :param entities: the MessageEntity's applied to the text.
    +    :return: a HTML representation of the combination of both inputs.
    +    """
    +    if not entities:
    +        return text
    +    html = []
    +    last_offset = 0
    +    for entity in entities:
    +        if entity.offset > last_offset:
    +            html.append(escape(text[last_offset:entity.offset]))
    +        elif entity.offset < last_offset:
    +            continue
    +
    +        skip_entity = False
    +        entity_text = escape(text[entity.offset:entity.offset + entity.length])
    +        entity_type = type(entity)
    +
    +        if entity_type == MessageEntityBold:
    +            html.append('{}'.format(entity_text))
    +        elif entity_type == MessageEntityItalic:
    +            html.append('{}'.format(entity_text))
    +        elif entity_type == MessageEntityCode:
    +            html.append('{}'.format(entity_text))
    +        elif entity_type == MessageEntityPre:
    +            if entity.language:
    +                html.append(
    +                    "
    \n"
    +                    "    \n"
    +                    "        {}\n"
    +                    "    \n"
    +                    "
    ".format(entity.language, entity_text)) + else: + html.append('
    {}
    ' + .format(entity_text)) + elif entity_type == MessageEntityEmail: + html.append('{0}'.format(entity_text)) + elif entity_type == MessageEntityUrl: + html.append('{0}'.format(entity_text)) + elif entity_type == MessageEntityTextUrl: + html.append('{}' + .format(escape(entity.url), entity_text)) + else: + skip_entity = True + last_offset = entity.offset + (0 if skip_entity else entity.length) + html.append(text[last_offset:]) + return ''.join(html) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5fe186f3..67644a7e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -70,7 +70,7 @@ from .tl.types import ( InputDocument, InputMediaDocument ) from .tl.types.messages import DialogsSlice -from .extensions import markdown +from .extensions import markdown, html __log__ = logging.getLogger(__name__) @@ -580,6 +580,8 @@ class TelegramClient(TelegramBareClient): parse_mode = parse_mode.lower() if parse_mode in {'md', 'markdown'}: message, msg_entities = markdown.parse(message) + elif parse_mode.startswith('htm'): + message, msg_entities = html.parse(message) else: raise ValueError('Unknown parsing mode: {}'.format(parse_mode)) else: From a437881ce235b1861a3e503a318fe23511228421 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 10:01:58 +0100 Subject: [PATCH 118/361] Note that date objects should be UTC --- docs/res/core.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/res/core.html b/docs/res/core.html index 0d1673aa..25295494 100644 --- a/docs/res/core.html +++ b/docs/res/core.html @@ -130,8 +130,12 @@ users.getUsers#0d91a548 id:Vector<InputUser> = Vector<User>
  • date: Although this type is internally used as an int, - you can pass a datetime object instead to work - with date parameters. + you can pass a datetime or date object + instead to work with date parameters.
    + Note that the library uses the date in UTC+0, since timezone + conversion is not responsibility of the library. Furthermore, this + eases converting into any other timezone without the need for a middle + step.
  • From f0eb41b90235c7931cf995084c25cfa4116f6907 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 11:59:35 +0100 Subject: [PATCH 119/361] Accept message/media on .send_file, remove redundancy off README --- README.rst | 2 +- telethon/telegram_client.py | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 6343e6e1..febc43cd 100755 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Telethon ⭐️ Thanks **everyone** who has starred the project, it means a lot! **Telethon** is Telegram client implementation in **Python 3** which uses -the latest available API of Telegram. Remember to use **pip3** to install! +the latest available API of Telegram. What is this? diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 67644a7e..73a5b66c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,4 +1,5 @@ import hashlib +import io import itertools import logging import os @@ -29,7 +30,6 @@ from .errors import ( SessionPasswordNeededError, FileMigrateError ) from .network import ConnectionMode -from .tl import TLObject from .tl.custom import Draft, Dialog from .tl.functions.account import ( GetPasswordRequest @@ -915,6 +915,22 @@ class TelegramClient(TelegramBareClient): ) for x in file ] + entity = self.get_input_entity(entity) + reply_to = self._get_reply_to(reply_to) + + if not isinstance(file, (str, bytes, io.IOBase)): + # The user may pass a Message containing media (or the media, + # or anything similar) that should be treated as a file. Try + # getting the input media for whatever they passed and send it. + try: + media = utils.get_input_media(file, user_caption=caption) + except TypeError: + pass # Can't turn whatever was given into media + else: + request = SendMediaRequest(entity, media, + reply_to_msg_id=reply_to) + return self._get_response_message(request, self(request)) + as_image = utils.is_image(file) and not force_document use_cache = InputPhoto if as_image else InputDocument file_handle = self.upload_file( @@ -979,11 +995,7 @@ class TelegramClient(TelegramBareClient): # Once the media type is properly specified and the file uploaded, # send the media message to the desired entity. - request = SendMediaRequest( - peer=self.get_input_entity(entity), - media=media, - reply_to_msg_id=self._get_reply_to(reply_to) - ) + request = SendMediaRequest(entity, media, reply_to_msg_id=reply_to) msg = self._get_response_message(request, self(request)) if msg and isinstance(file_handle, InputSizedFile): # There was a response message and we didn't use cached From 81c95b5a607cc16e00732d0e0113eece5873c016 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 12:04:35 +0100 Subject: [PATCH 120/361] Fix recursive .get_input_media() forgetting parameters --- telethon/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index d113da73..16257be2 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -282,9 +282,10 @@ def get_input_media(media, user_caption=None, is_photo=False): if isinstance(media, (ChatPhoto, UserProfilePhoto)): if isinstance(media.photo_big, FileLocationUnavailable): - return get_input_media(media.photo_small, is_photo=True) + media = media.photo_small else: - return get_input_media(media.photo_big, is_photo=True) + media = media.photo_big + return get_input_media(media, user_caption=user_caption, is_photo=True) if isinstance(media, MessageMediaContact): return InputMediaContact( @@ -312,7 +313,9 @@ def get_input_media(media, user_caption=None, is_photo=False): return InputMediaEmpty() if isinstance(media, Message): - return get_input_media(media.media) + return get_input_media( + media.media, user_caption=user_caption, is_photo=is_photo + ) _raise_cast_fail(media, 'InputMedia') From 58d90e7e340486bb89cc09bf2573cd4be77c4f37 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 12:10:23 +0100 Subject: [PATCH 121/361] Fix .download_media() not accepting Document --- telethon/telegram_client.py | 15 +++++++++------ telethon/utils.py | 14 +++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 73a5b66c..ad917f71 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -67,7 +67,7 @@ from .tl.types import ( PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, - InputDocument, InputMediaDocument + InputDocument, InputMediaDocument, Document ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -1308,7 +1308,7 @@ class TelegramClient(TelegramBareClient): return self._download_photo( media, file, date, progress_callback ) - elif isinstance(media, MessageMediaDocument): + elif isinstance(media, (MessageMediaDocument, Document)): return self._download_document( media, file, date, progress_callback ) @@ -1319,7 +1319,6 @@ class TelegramClient(TelegramBareClient): def _download_photo(self, photo, file, date, progress_callback): """Specialized version of .download_media() for photos""" - # Determine the photo and its largest size if isinstance(photo, MessageMediaPhoto): photo = photo.photo @@ -1345,9 +1344,13 @@ class TelegramClient(TelegramBareClient): ) return file - def _download_document(self, mm_doc, file, date, progress_callback): + def _download_document(self, document, file, date, progress_callback): """Specialized version of .download_media() for documents""" - document = mm_doc.document + if isinstance(document, MessageMediaDocument): + document = document.document + if not isinstance(document, Document): + return + file_size = document.size possible_names = [] @@ -1361,7 +1364,7 @@ class TelegramClient(TelegramBareClient): )) file = self._get_proper_filename( - file, 'document', utils.get_extension(mm_doc), + file, 'document', utils.get_extension(document), date=date, possible_names=possible_names ) diff --git a/telethon/utils.py b/telethon/utils.py index 16257be2..3e310d3d 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -61,13 +61,13 @@ def get_extension(media): # Documents will come with a mime type if isinstance(media, MessageMediaDocument): - if isinstance(media.document, Document): - if media.document.mime_type == 'application/octet-stream': - # Octet stream are just bytes, which have no default extension - return '' - else: - extension = guess_extension(media.document.mime_type) - return extension if extension else '' + media = media.document + if isinstance(media, Document): + if media.mime_type == 'application/octet-stream': + # Octet stream are just bytes, which have no default extension + return '' + else: + return guess_extension(media.mime_type) or '' return '' From 32b92b32a7dfd808c500a1a63b6a0e868ebff659 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 12:13:03 +0100 Subject: [PATCH 122/361] Update .send_file() documentation (for f0eb41b) --- telethon/telegram_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ad917f71..e0a90d23 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -851,7 +851,7 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): Who will receive the file. - file (:obj:`str` | :obj:`bytes` | :obj:`file`): + file (:obj:`str` | :obj:`bytes` | :obj:`file` | :obj:`media`): The path of the file, byte array, or stream that will be sent. 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 @@ -860,6 +860,10 @@ class TelegramClient(TelegramBareClient): Subsequent calls with the very same file will result in immediate uploads, unless ``.clear_file_cache()`` is called. + Furthermore the file may be any media (a message, document, + photo or similar) so that it can be resent without the need + to download and re-upload it again. + caption (:obj:`str`, optional): Optional caption for the sent media message. From 6c73538bd412717143b7fad459f529f81807b4fa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 11:39:43 +0100 Subject: [PATCH 123/361] Fix time_offset not being used at all after BadMsgNotification Telegram would refuse to reply any further unless the message ID had the correct time (causing some behaviour like .connect() never connecting, due to the first request being sent always failing). The fix was to use time_offset when calculating the message ID, while this was right, it wasn't in use. --- telethon/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/session.py b/telethon/session.py index 21c0e105..5657f339 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -311,7 +311,7 @@ class Session: now = time.time() nanoseconds = int((now - int(now)) * 1e+9) # "message identifiers are divisible by 4" - new_msg_id = (int(now) << 32) | (nanoseconds << 2) + new_msg_id = ((int(now) + self.time_offset) << 32) | (nanoseconds << 2) with self._msg_id_lock: if self._last_msg_id >= new_msg_id: From db698858e0183692c993e20e69cf7fa31f972ffe Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 22:25:52 +0100 Subject: [PATCH 124/361] Except TypeNotFoundError on ._invoke() --- telethon/telegram_bare_client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 9b756f43..11a8c184 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -501,6 +501,15 @@ class TelegramBareClient: __log__.error('Authorization key seems broken and was invalid!') self.session.auth_key = None + except TypeNotFoundError as e: + # Only occurs when we call receive. May happen when + # we need to reconnect to another DC on login and + # Telegram somehow sends old objects (like configOld) + self._first_request = True + __log__.warning('Read unknown TLObject code ({}). ' + 'Setting again first_request flag.' + .format(hex(e.invalid_constructor_id))) + except TimeoutError: __log__.warning('Invoking timed out') # We will just retry From 2873dcf1c6e914c026c2ed4259bcfc0f89c9026c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 25 Jan 2018 09:44:07 +0100 Subject: [PATCH 125/361] Add '_' key to TLObject's .to_dict() and remove recursive param Closes #559 --- telethon/tl/tlobject.py | 54 +++++++++++++++++------------- telethon_generator/tl_generator.py | 21 ++++++------ 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index ac0b65f8..87edd83e 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -20,15 +20,18 @@ class TLObject: """ if indent is None: if isinstance(obj, TLObject): - return '{}({})'.format(type(obj).__name__, ', '.join( - '{}={}'.format(k, TLObject.pretty_format(v)) - for k, v in obj.to_dict(recursive=False).items() - )) + obj = obj.to_dict() + if isinstance(obj, dict): - return '{{{}}}'.format(', '.join( - '{}: {}'.format(k, TLObject.pretty_format(v)) - for k, v in obj.items() - )) + if '_' in obj: + pre, left, right, sep = obj['_'], '(', ')', '{}={}' + else: + pre, left, right, sep = '', '{', '}', '{}: {}' + + mid = ', '.join(sep.format(k, TLObject.pretty_format(v)) + for k, v in obj.items() if not pre or k != '_') + return '{}{}{}{}'.format(pre, left, mid, right) + elif isinstance(obj, str) or isinstance(obj, bytes): return repr(obj) elif hasattr(obj, '__iter__'): @@ -43,30 +46,33 @@ class TLObject: return repr(obj) else: result = [] - 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__) + if isinstance(obj, TLObject): + obj = obj.to_dict() - result.append(start) - if d: + if isinstance(obj, dict): + if '_' in obj: + pre, left, right, sep = obj['_'], '(', ')', '{}={}' + else: + pre, left, right, sep = '', '{', '}', '{}: {}' + + result.append(pre) + result.append(left) + if obj: result.append('\n') indent += 1 - for k, v in d.items(): + for k, v in obj.items(): + if pre and k == '_': + continue result.append('\t' * indent) - result.append(k) - result.append(sep) - result.append(TLObject.pretty_format(v, indent)) + result.append(sep.format( + k, TLObject.pretty_format(v, indent) + )) result.append(',\n') result.pop() # last ',\n' indent -= 1 result.append('\n') result.append('\t' * indent) - result.append(end) + result.append(right) elif isinstance(obj, str) or isinstance(obj, bytes): result.append(repr(obj)) @@ -158,7 +164,7 @@ class TLObject: def resolve(self, client, utils): pass - def to_dict(self, recursive=True): + def to_dict(self): return {} def __bytes__(self): diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 18293ba1..ff12acfe 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -297,17 +297,16 @@ class TLGenerator: builder.end_block() # Write the to_dict(self) method - builder.writeln('def to_dict(self, recursive=True):') - if args: - builder.writeln('return {') - else: - builder.write('return {') + builder.writeln('def to_dict(self):') + builder.writeln('return {') builder.current_indent += 1 base_types = ('string', 'bytes', 'int', 'long', 'int128', 'int256', 'double', 'Bool', 'true', 'date') + builder.write("'_': '{}'".format(tlobject.class_name())) for arg in args: + builder.writeln(',') builder.write("'{}': ".format(arg.name)) if arg.type in base_types: if arg.is_vector: @@ -318,17 +317,17 @@ class TLGenerator: else: if arg.is_vector: builder.write( - '([] if self.{0} is None else [None' - ' if x is None else x.to_dict() for x in self.{0}]' - ') if recursive else self.{0}'.format(arg.name) + '[] if self.{0} is None else [None ' + 'if x is None else x.to_dict() for x in self.{0}]' + .format(arg.name) ) else: builder.write( - '(None if self.{0} is None else self.{0}.to_dict())' - ' if recursive else self.{0}'.format(arg.name) + 'None if self.{0} is None else self.{0}.to_dict()' + .format(arg.name) ) - builder.writeln(',') + builder.writeln() builder.current_indent -= 1 builder.writeln("}") From 4a83784fe8ebe3d8dcf041369f41d850f44549f3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 25 Jan 2018 09:51:12 +0100 Subject: [PATCH 126/361] Simplify TLObject.pretty_format since Telegram returns no dicts --- telethon/tl/tlobject.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 87edd83e..db1982c4 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -23,15 +23,10 @@ class TLObject: obj = obj.to_dict() if isinstance(obj, dict): - if '_' in obj: - pre, left, right, sep = obj['_'], '(', ')', '{}={}' - else: - pre, left, right, sep = '', '{', '}', '{}: {}' - - mid = ', '.join(sep.format(k, TLObject.pretty_format(v)) - for k, v in obj.items() if not pre or k != '_') - return '{}{}{}{}'.format(pre, left, mid, right) - + return '{}({})'.format(obj.get('_', 'dict'), ', '.join( + '{}={}'.format(k, TLObject.pretty_format(v)) + for k, v in obj.items() if k != '_' + )) elif isinstance(obj, str) or isinstance(obj, bytes): return repr(obj) elif hasattr(obj, '__iter__'): @@ -50,29 +45,24 @@ class TLObject: obj = obj.to_dict() if isinstance(obj, dict): - if '_' in obj: - pre, left, right, sep = obj['_'], '(', ')', '{}={}' - else: - pre, left, right, sep = '', '{', '}', '{}: {}' - - result.append(pre) - result.append(left) + result.append(obj.get('_', 'dict')) + result.append('(') if obj: result.append('\n') indent += 1 for k, v in obj.items(): - if pre and k == '_': + if k == '_': continue result.append('\t' * indent) - result.append(sep.format( - k, TLObject.pretty_format(v, indent) - )) + result.append(k) + result.append('=') + 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(right) + result.append(')') elif isinstance(obj, str) or isinstance(obj, bytes): result.append(repr(obj)) From 5c2dfc17a8b8b0fc636d9aea199e41f9c41ab71e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 25 Jan 2018 18:44:21 +0100 Subject: [PATCH 127/361] Make timeout logging message debug to scare people less --- 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 11a8c184..c2955469 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -658,7 +658,7 @@ class TelegramBareClient: self._sender.receive(update_state=self.updates) except TimeoutError: # No problem - __log__.info('Receiving items from the network timed out') + __log__.debug('Receiving items from the network timed out') except ConnectionResetError: if self._user_connected: __log__.error('Connection was reset while receiving ' From 43a3f405271560d705d5c3cc1f7ffb647ce477b9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 26 Jan 2018 09:59:49 +0100 Subject: [PATCH 128/361] Properly close the sqlite3 connection (#560) --- telethon/session.py | 42 ++++++++++++++++++++++---------- telethon/telegram_bare_client.py | 1 + 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/telethon/session.py b/telethon/session.py index 5657f339..266d732e 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -7,7 +7,7 @@ import time from base64 import b64decode from enum import Enum from os.path import isfile as file_exists -from threading import Lock +from threading import Lock, RLock from . import utils from .crypto import AuthKey @@ -89,7 +89,7 @@ class Session: # Cross-thread safety self._seq_no_lock = Lock() self._msg_id_lock = Lock() - self._db_lock = Lock() + self._db_lock = RLock() # These values will be saved self._dc_id = 0 @@ -100,8 +100,8 @@ class Session: # Migrating from .json -> SQL entities = self._check_migrate_json() - self._conn = sqlite3.connect(self.filename, check_same_thread=False) - c = self._conn.cursor() + self._conn = None + c = self._cursor() c.execute("select name from sqlite_master " "where type='table' and name='version'") if c.fetchone(): @@ -186,7 +186,7 @@ class Session: return [] # No entities def _upgrade_database(self, old): - c = self._conn.cursor() + c = self._cursor() # old == 1 doesn't have the old sent_files so no need to drop if old == 2: # Old cache from old sent_files lasts then a day anyway, drop @@ -223,7 +223,7 @@ class Session: self._update_session_table() # Fetch the auth_key corresponding to this data center - c = self._conn.cursor() + c = self._cursor() c.execute('select auth_key from sessions') tuple_ = c.fetchone() if tuple_: @@ -251,7 +251,7 @@ class Session: def _update_session_table(self): with self._db_lock: - c = self._conn.cursor() + c = self._cursor() # While we can save multiple rows into the sessions table # currently we only want to keep ONE as the tables don't # tell us which auth_key's are usable and will work. Needs @@ -271,6 +271,22 @@ class Session: with self._db_lock: self._conn.commit() + def _cursor(self): + """Asserts that the connection is open and returns a cursor""" + with self._db_lock: + if self._conn is None: + self._conn = sqlite3.connect(self.filename, + check_same_thread=False) + return self._conn.cursor() + + def close(self): + """Closes the connection unless we're working in-memory""" + if self.filename != ':memory:': + with self._db_lock: + if self._conn is not None: + self._conn.close() + self._conn = None + def delete(self): """Deletes the current session file""" if self.filename == ':memory:': @@ -385,10 +401,10 @@ class Session: return with self._db_lock: - self._conn.executemany( + self._cursor().executemany( 'insert or replace into entities values (?,?,?,?,?)', rows ) - self.save() + self.save() def get_input_entity(self, key): """Parses the given string, integer or TLObject key into a @@ -413,7 +429,7 @@ class Session: if isinstance(key, TLObject): key = utils.get_peer_id(key) - c = self._conn.cursor() + c = self._cursor() if isinstance(key, str): phone = utils.parse_phone(key) if phone: @@ -444,7 +460,7 @@ class Session: # File processing def get_file(self, md5_digest, file_size, cls): - tuple_ = self._conn.execute( + tuple_ = self._cursor().execute( 'select id, hash from sent_files ' 'where md5_digest = ? and file_size = ? and type = ?', (md5_digest, file_size, _SentFileType.from_type(cls).value) @@ -458,10 +474,10 @@ class Session: raise TypeError('Cannot cache %s instance' % type(instance)) with self._db_lock: - self._conn.execute( + self._cursor().execute( 'insert or replace into sent_files values (?,?,?,?,?)', ( md5_digest, file_size, _SentFileType.from_type(type(instance)).value, instance.id, instance.access_hash )) - self.save() + self.save() diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index c2955469..fe63ab8a 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -253,6 +253,7 @@ class TelegramBareClient: # TODO Shall we clear the _exported_sessions, or may be reused? self._first_request = True # On reconnect it will be first again + self.session.close() def __del__(self): self.disconnect() From 3b8365f8716add070490e7aca46b728eb8cbc43b Mon Sep 17 00:00:00 2001 From: frizzlywitch Date: Fri, 26 Jan 2018 16:38:13 +0300 Subject: [PATCH 129/361] Remove square braces from IPv6 addresses (#561) --- telethon/extensions/tcp_client.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index d01c2b13..d4c45776 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -56,12 +56,7 @@ class TcpClient: :param port: the port to connect to. """ if ':' in ip: # IPv6 - # The address needs to be surrounded by [] as discussed on PR#425 - if not ip.startswith('['): - ip = '[' + ip - if not ip.endswith(']'): - ip = ip + ']' - + ip = ip.replace('[', '').replace(']', '') mode, address = socket.AF_INET6, (ip, port, 0, 0) else: mode, address = socket.AF_INET, (ip, port) From 067006d24833e592fad6183fdd8a805182dd4b10 Mon Sep 17 00:00:00 2001 From: Matteo Date: Sat, 27 Jan 2018 15:29:38 -0500 Subject: [PATCH 130/361] Add batch_size and wait_time to get_message_history (#565) --- telethon/telegram_client.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e0a90d23..c2a91de7 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -646,7 +646,8 @@ class TelegramClient(TelegramBareClient): return self(messages.DeleteMessagesRequest(message_ids, revoke=revoke)) def get_message_history(self, entity, limit=20, offset_date=None, - offset_id=0, max_id=0, min_id=0, add_offset=0): + offset_id=0, max_id=0, min_id=0, add_offset=0, + batch_size=100, wait_time=1): """ Gets the message history for the specified entity @@ -681,6 +682,15 @@ class TelegramClient(TelegramBareClient): Additional message offset (all of the specified offsets + this offset = older messages). + batch_size (:obj:`int`): + Number of messages to be returned by each Telegram API + "getHistory" request. Notice that Telegram has a hard limit + of 100 messages per API call. + + wait_time (:obj:`int`): + Wait time between different "getHistory" requests. Use this + parameter to avoid hitting the "FloodWaitError" (see note below). + Returns: A list of messages with extra attributes: @@ -689,6 +699,16 @@ class TelegramClient(TelegramBareClient): * ``.fwd_from.sender`` = if fwd_from, who sent it originally. * ``.fwd_from.channel`` = if fwd_from, original channel. * ``.to`` = entity to which the message was sent. + + Notes: + Telegram limit for "getHistory" requests seems to be 3000 messages + within 30 seconds. Therefore, please adjust "batch_size" and + "wait_time" parameters accordingly to avoid incurring into a + "FloodWaitError". For example, if you plan to retrieve more than 3000 + messages (i.e. limit=3000 or None) in batches of 100 messages + (i.e. batch_size=100) please make sure to select a wait time of at + least one second (i.e. wait_time=1). + """ entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) @@ -705,7 +725,7 @@ class TelegramClient(TelegramBareClient): entities = {} while len(messages) < limit: # Telegram has a hard limit of 100 - real_limit = min(limit - len(messages), 100) + real_limit = min(limit - len(messages), min(batch_size,100)) result = self(GetHistoryRequest( peer=entity, limit=real_limit, @@ -738,8 +758,7 @@ class TelegramClient(TelegramBareClient): # batches of 100 messages each request (since the FloodWait was # of 30 seconds). If the limit is greater than that, we will # sleep 1s between each request. - if limit > 3000: - time.sleep(1) + time.sleep(wait_time) # Add a few extra attributes to the Message to make it friendlier. messages.total = total_messages From 700b4c3169a7dbf64c64191ec43756b044428a06 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 27 Jan 2018 21:37:57 +0100 Subject: [PATCH 131/361] Fix-up #565 with some rewording/behaviour changes Such as not waiting unless strictly needed and better wording. --- telethon/telegram_client.py | 39 +++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c2a91de7..18602032 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -647,7 +647,7 @@ class TelegramClient(TelegramBareClient): def get_message_history(self, entity, limit=20, offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0, - batch_size=100, wait_time=1): + batch_size=100, wait_time=None): """ Gets the message history for the specified entity @@ -683,13 +683,15 @@ class TelegramClient(TelegramBareClient): this offset = older messages). batch_size (:obj:`int`): - Number of messages to be returned by each Telegram API - "getHistory" request. Notice that Telegram has a hard limit - of 100 messages per API call. + Messages will be returned in chunks of this size (100 is + the maximum). While it makes no sense to modify this value, + you are still free to do so. wait_time (:obj:`int`): - Wait time between different "getHistory" requests. Use this - parameter to avoid hitting the "FloodWaitError" (see note below). + Wait time between different ``GetHistoryRequest``. Use this + parameter to avoid hitting the ``FloodWaitError`` as needed. + If left to ``None``, it will default to 1 second only if + the limit is higher than 3000. Returns: A list of messages with extra attributes: @@ -701,13 +703,11 @@ class TelegramClient(TelegramBareClient): * ``.to`` = entity to which the message was sent. Notes: - Telegram limit for "getHistory" requests seems to be 3000 messages - within 30 seconds. Therefore, please adjust "batch_size" and - "wait_time" parameters accordingly to avoid incurring into a - "FloodWaitError". For example, if you plan to retrieve more than 3000 - messages (i.e. limit=3000 or None) in batches of 100 messages - (i.e. batch_size=100) please make sure to select a wait time of at - least one second (i.e. wait_time=1). + Telegram's flood wait limit for ``GetHistoryRequest`` seems to + be around 30 seconds per 3000 messages, therefore a sleep of 1 + second is the default for this limit (or above). You may need + an higher limit, so you're free to set the ``batch_size`` that + you think may be good. """ entity = self.get_input_entity(entity) @@ -720,12 +720,16 @@ class TelegramClient(TelegramBareClient): )) return getattr(result, 'count', len(result.messages)), [], [] + if wait_time is None: + wait_time = 1 if limit > 3000 else 0 + + batch_size = min(max(batch_size, 1), 100) total_messages = 0 messages = UserList() entities = {} while len(messages) < limit: # Telegram has a hard limit of 100 - real_limit = min(limit - len(messages), min(batch_size,100)) + real_limit = min(limit - len(messages), batch_size) result = self(GetHistoryRequest( peer=entity, limit=real_limit, @@ -741,8 +745,6 @@ class TelegramClient(TelegramBareClient): ) total_messages = getattr(result, 'count', len(result.messages)) - # TODO We can potentially use self.session.database, but since - # it might be disabled, use a local dictionary. for u in result.users: entities[utils.get_peer_id(u)] = u for c in result.chats: @@ -753,11 +755,6 @@ class TelegramClient(TelegramBareClient): offset_id = result.messages[-1].id offset_date = result.messages[-1].date - - # Telegram limit seems to be 3000 messages within 30 seconds in - # batches of 100 messages each request (since the FloodWait was - # of 30 seconds). If the limit is greater than that, we will - # sleep 1s between each request. time.sleep(wait_time) # Add a few extra attributes to the Message to make it friendlier. From 7286f77008af13d959a7a553a45d202a0ac305f2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 28 Jan 2018 14:02:42 +0100 Subject: [PATCH 132/361] Sort keys and use Mozilla agent on error generator, update file --- telethon_generator/error_generator.py | 16 +++++++++++++--- telethon_generator/errors.json | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py index 5b14f22e..a56d4b91 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -26,7 +26,9 @@ known_codes = { def fetch_errors(output, url=URL): print('Opening a connection to', url, '...') - r = urllib.request.urlopen(url) + r = urllib.request.urlopen(urllib.request.Request( + url, headers={'User-Agent' : 'Mozilla/5.0'} + )) print('Checking response...') data = json.loads( r.read().decode(r.info().get_param('charset') or 'utf-8') @@ -34,11 +36,11 @@ def fetch_errors(output, url=URL): if data.get('ok'): print('Response was okay, saving data') with open(output, 'w', encoding='utf-8') as f: - json.dump(data, f) + json.dump(data, f, sort_keys=True) return True else: print('The data received was not okay:') - print(json.dumps(data, indent=4)) + print(json.dumps(data, indent=4, sort_keys=True)) return False @@ -164,3 +166,11 @@ def generate_code(output, json_file, errors_desc): for pattern, name in patterns: f.write(' {}: {},\n'.format(repr(pattern), name)) f.write('}\n') + + +if __name__ == '__main__': + if input('generate (y/n)?: ').lower() == 'y': + generate_code('../telethon/errors/rpc_error_list.py', + 'errors.json', 'error_descriptions') + elif input('fetch (y/n)?: ').lower() == 'y': + fetch_errors('errors.json') diff --git a/telethon_generator/errors.json b/telethon_generator/errors.json index e807ff2d..31d31c0c 100644 --- a/telethon_generator/errors.json +++ b/telethon_generator/errors.json @@ -1 +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 +{"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"], "ADMINS_TOO_MUCH": ["Too many admins"], "API_ID_INVALID": ["The api_id/api_hash combination is invalid"], "API_ID_PUBLISHED_FLOOD": ["This API id was published somewhere, you can't use it now"], "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"], "BOTS_TOO_MUCH": ["There are too many bots in this chat/channel"], "BOT_CHANNELS_NA": ["Bots can't edit admin privileges"], "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"], "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"], "CHANNELS_ADMIN_PUBLIC_TOO_MUCH": ["You're admin of too many public channels, make some channels private to change the username of this channel"], "CHANNELS_TOO_MUCH": ["You have joined too many channels/supergroups"], "CHANNEL_INVALID": ["The provided channel is invalid"], "CHANNEL_PRIVATE": ["You haven't joined this channel/supergroup"], "CHANNEL_PUBLIC_GROUP_NA": ["channel/supergroup not available"], "CHAT_ABOUT_NOT_MODIFIED": ["About text has not changed"], "CHAT_ABOUT_TOO_LONG": ["Chat about too long"], "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_DEVICE_MODEL_EMPTY": ["Device model empty"], "CONNECTION_LANG_PACK_INVALID": ["Language pack invalid"], "CONNECTION_LAYER_INVALID": ["Layer invalid"], "CONNECTION_NOT_INITED": ["Connection not initialized"], "CONNECTION_SYSTEM_EMPTY": ["Connection system empty"], "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"], "EMAIL_UNCONFIRMED": ["Email unconfirmed"], "ENCRYPTED_MESSAGE_INVALID": ["Encrypted message invalid"], "ENCRYPTION_ALREADY_ACCEPTED": ["Secret chat already accepted"], "ENCRYPTION_ALREADY_DECLINED": ["The secret chat was already declined"], "ENCRYPTION_DECLINED": ["The secret chat was 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"], "EXTERNAL_URL_INVALID": ["External URL 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_PARTS_INVALID": ["The number of file parts is invalid"], "FILE_PART_0_MISSING": ["File part 0 missing"], "FILE_PART_122_MISSING": [""], "FILE_PART_154_MISSING": [""], "FILE_PART_458_MISSING": [""], "FILE_PART_468_MISSING": [""], "FILE_PART_504_MISSING": [""], "FILE_PART_6_MISSING": ["File part 6 missing"], "FILE_PART_72_MISSING": [""], "FILE_PART_94_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"], "FIRSTNAME_INVALID": ["The first name is invalid"], "FLOOD_WAIT_666": ["Spooky af m8"], "GIF_ID_INVALID": ["The provided GIF ID is invalid"], "GROUPED_MEDIA_INVALID": ["Invalid grouped media"], "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"], "MEDIA_INVALID": ["Media invalid"], "MEMBER_NO_LOCATION": ["An internal failure occurred while fetching user info (couldn't find location)"], "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_IDS_EMPTY": ["No message ids were provided"], "MESSAGE_ID_INVALID": ["The provided message id is invalid"], "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"], "PACK_SHORT_NAME_OCCUPIED": ["A stickerpack with this name already exists"], "PARTICIPANTS_TOO_FEW": ["Not enough participants"], "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"], "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": ["Photo 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"], "STICKERSET_INVALID": ["The provided sticker set is invalid"], "STICKERS_EMPTY": ["No sticker provided"], "STICKER_EMOJI_INVALID": ["Sticker emoji invalid"], "STICKER_FILE_INVALID": ["Sticker file invalid"], "STICKER_ID_INVALID": ["The provided sticker ID is invalid"], "STICKER_INVALID": ["The provided sticker is invalid"], "STICKER_PNG_DIMENSIONS": ["Sticker png dimensions invalid"], "STORAGE_CHECK_FAILED": ["Server storage check failed"], "STORE_INVALID_SCALAR_TYPE": [""], "TEMP_AUTH_KEY_EMPTY": ["No temporary auth key provided"], "TMP_PASSWORD_DISABLED": ["The temporary password is disabled"], "TOKEN_INVALID": ["The provided token is invalid"], "TTL_DAYS_INVALID": ["The provided TTL is invalid"], "TYPES_EMPTY": ["The types field is empty"], "TYPE_CONSTRUCTOR_INVALID": ["The type constructor is invalid"], "Timeout": ["A timeout occurred while fetching data from the bot"], "UNKNOWN_METHOD": ["The method you tried to call cannot be called on non-CDN DCs"], "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)"], "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."], "WC_CONVERT_URL_INVALID": ["WC convert URL invalid"], "WEBPAGE_CURL_FAILED": ["Failure while fetching the webpage with cURL"], "WEBPAGE_MEDIA_EMPTY": ["Webpage media empty"], "YOU_BLOCKED_USER": ["You blocked this user"]}, "ok": true, "result": {"-503": {"auth.bindTempAuthKey": ["Timeout"], "auth.resetAuthorizations": ["Timeout"], "channels.getFullChannel": ["Timeout"], "channels.getParticipants": ["Timeout"], "contacts.deleteContacts": ["Timeout"], "contacts.search": ["Timeout"], "help.getCdnConfig": ["Timeout"], "help.getConfig": ["Timeout"], "messages.forwardMessages": ["Timeout"], "messages.getBotCallbackAnswer": ["Timeout"], "messages.getDialogs": ["Timeout"], "messages.getHistory": ["Timeout"], "messages.getInlineBotResults": ["Timeout"], "messages.readHistory": ["Timeout"], "messages.sendMedia": ["Timeout"], "messages.sendMessage": ["Timeout"], "updates.getChannelDifference": ["Timeout"], "updates.getDifference": ["Timeout"], "updates.getState": ["Timeout"], "upload.getFile": ["Timeout"], "users.getFullUser": ["Timeout"], "users.getUsers": ["Timeout"]}, "400": {"account.changePhone": ["PHONE_NUMBER_INVALID"], "account.checkUsername": ["USERNAME_INVALID"], "account.confirmPhone": ["CODE_HASH_INVALID", "PHONE_CODE_EMPTY"], "account.getNotifySettings": ["PEER_ID_INVALID"], "account.getPasswordSettings": ["PASSWORD_HASH_INVALID"], "account.getPrivacy": ["PRIVACY_KEY_INVALID"], "account.getTmpPassword": ["PASSWORD_HASH_INVALID", "TMP_PASSWORD_DISABLED"], "account.registerDevice": ["TOKEN_INVALID"], "account.reportPeer": ["PEER_ID_INVALID"], "account.resetAuthorization": ["HASH_INVALID"], "account.sendChangePhoneCode": ["PHONE_NUMBER_INVALID"], "account.sendConfirmPhoneCode": ["HASH_INVALID"], "account.setAccountTTL": ["TTL_DAYS_INVALID"], "account.setPrivacy": ["PRIVACY_KEY_INVALID"], "account.unregisterDevice": ["TOKEN_INVALID"], "account.updateNotifySettings": ["PEER_ID_INVALID"], "account.updatePasswordSettings": ["EMAIL_UNCONFIRMED", "NEW_SALT_INVALID", "NEW_SETTINGS_INVALID", "PASSWORD_HASH_INVALID"], "account.updateProfile": ["ABOUT_TOO_LONG", "FIRSTNAME_INVALID"], "account.updateUsername": ["USERNAME_INVALID", "USERNAME_NOT_MODIFIED", "USERNAME_OCCUPIED"], "auth.bindTempAuthKey": ["ENCRYPTED_MESSAGE_INVALID", "INPUT_REQUEST_TOO_LONG", "TEMP_AUTH_KEY_EMPTY"], "auth.cancelCode": ["PHONE_NUMBER_INVALID"], "auth.checkPassword": ["PASSWORD_HASH_INVALID"], "auth.checkPhone": ["PHONE_NUMBER_BANNED", "PHONE_NUMBER_INVALID"], "auth.exportAuthorization": ["DC_ID_INVALID"], "auth.importAuthorization": ["AUTH_BYTES_INVALID", "USER_ID_INVALID"], "auth.importBotAuthorization": ["ACCESS_TOKEN_EXPIRED", "ACCESS_TOKEN_INVALID", "API_ID_INVALID"], "auth.recoverPassword": ["CODE_EMPTY"], "auth.requestPasswordRecovery": ["PASSWORD_EMPTY"], "auth.resendCode": ["PHONE_NUMBER_INVALID"], "auth.sendCode": ["API_ID_INVALID", "API_ID_PUBLISHED_FLOOD", "INPUT_REQUEST_TOO_LONG", "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN", "PHONE_NUMBER_BANNED", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_PASSWORD_PROTECTED"], "auth.sendInvites": ["MESSAGE_EMPTY"], "auth.signIn": ["PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_UNOCCUPIED"], "auth.signUp": ["FIRSTNAME_INVALID", "PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_OCCUPIED"], "bots.answerWebhookJSONQuery": ["QUERY_ID_INVALID", "USER_BOT_INVALID"], "bots.sendCustomRequest": ["USER_BOT_INVALID"], "channels.checkUsername": ["CHANNEL_INVALID", "CHAT_ID_INVALID", "USERNAME_INVALID"], "channels.createChannel": ["CHAT_TITLE_EMPTY"], "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": ["ADMINS_TOO_MUCH", "BOT_CHANNELS_NA", "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", "CHAT_ADMIN_REQUIRED", "PHOTO_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", "CHANNEL_PRIVATE", "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", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "USER_ID_INVALID", "USER_NOT_PARTICIPANT"], "channels.getParticipants": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_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"], "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", "INPUT_USER_DEACTIVATED"], "channels.setStickers": ["CHANNEL_INVALID", "PARTICIPANTS_TOO_FEW"], "channels.toggleInvites": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.toggleSignatures": ["CHANNEL_INVALID"], "channels.updatePinnedMessage": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "channels.updateUsername": ["CHANNEL_INVALID", "CHANNELS_ADMIN_PUBLIC_TOO_MUCH", "CHAT_ADMIN_REQUIRED", "USERNAME_INVALID", "USERNAME_OCCUPIED"], "contacts.block": ["CONTACT_ID_INVALID"], "contacts.deleteContact": ["CONTACT_ID_INVALID"], "contacts.getTopPeers": ["TYPES_EMPTY"], "contacts.importCard": ["EXPORT_CARD_INVALID"], "contacts.resetTopPeerRating": ["PEER_ID_INVALID"], "contacts.resolveUsername": ["USERNAME_INVALID", "USERNAME_NOT_OCCUPIED"], "contacts.search": ["QUERY_TOO_SHORT", "SEARCH_QUERY_EMPTY"], "contacts.unblock": ["CONTACT_ID_INVALID"], "initConnection": ["CONNECTION_LAYER_INVALID", "INPUT_FETCH_FAIL"], "invokeWithLayer": ["AUTH_BYTES_INVALID", "CDN_METHOD_INVALID", "CONNECTION_API_ID_INVALID", "CONNECTION_DEVICE_MODEL_EMPTY", "CONNECTION_LANG_PACK_INVALID", "CONNECTION_NOT_INITED", "CONNECTION_SYSTEM_EMPTY", "INPUT_LAYER_INVALID", "INVITE_HASH_EXPIRED"], "langpack.getDifference": ["LANG_PACK_INVALID"], "langpack.getLangPack": ["LANG_PACK_INVALID"], "langpack.getLanguages": ["LANG_PACK_INVALID"], "langpack.getStrings": ["LANG_PACK_INVALID"], "messages.acceptEncryption": ["CHAT_ID_INVALID", "ENCRYPTION_ALREADY_ACCEPTED", "ENCRYPTION_ALREADY_DECLINED"], "messages.addChatUser": ["CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "USER_ALREADY_PARTICIPANT", "USER_ID_INVALID", "USERS_TOO_MUCH"], "messages.checkChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID"], "messages.createChat": ["USERS_TOO_FEW"], "messages.deleteChatUser": ["CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_NOT_PARTICIPANT"], "messages.deleteHistory": ["PEER_ID_INVALID"], "messages.discardEncryption": ["CHAT_ID_EMPTY", "ENCRYPTION_ALREADY_DECLINED", "ENCRYPTION_ID_INVALID"], "messages.editChatAdmin": ["CHAT_ID_INVALID"], "messages.editChatPhoto": ["CHAT_ID_INVALID", "INPUT_CONSTRUCTOR_INVALID", "INPUT_FETCH_FAIL", "PEER_ID_INVALID", "PHOTO_EXT_INVALID"], "messages.editChatTitle": ["CHAT_ID_INVALID"], "messages.editInlineBotMessage": ["MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED"], "messages.editMessage": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "INPUT_USER_DEACTIVATED", "MESSAGE_EDIT_TIME_EXPIRED", "MESSAGE_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED", "PEER_ID_INVALID"], "messages.exportChatInvite": ["CHAT_ID_INVALID"], "messages.faveSticker": ["STICKER_ID_INVALID"], "messages.forwardMessage": ["CHAT_ID_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID", "YOU_BLOCKED_USER"], "messages.forwardMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "GROUPED_MEDIA_INVALID", "INPUT_USER_DEACTIVATED", "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.getBotCallbackAnswer": ["CHANNEL_INVALID", "DATA_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID"], "messages.getChats": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getCommonChats": ["USER_ID_INVALID"], "messages.getDhConfig": ["RANDOM_LENGTH_INVALID"], "messages.getDialogs": ["INPUT_CONSTRUCTOR_INVALID", "OFFSET_PEER_ID_INVALID"], "messages.getDocumentByHash": ["SHA256_HASH_INVALID"], "messages.getFullChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getGameHighScores": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getInlineBotResults": ["BOT_INLINE_DISABLED", "BOT_INVALID", "CHANNEL_PRIVATE"], "messages.getInlineGameHighScores": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getMessageEditData": ["PEER_ID_INVALID"], "messages.getMessagesViews": ["CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerDialogs": ["CHANNEL_PRIVATE", "PEER_ID_INVALID"], "messages.getPeerSettings": ["CHANNEL_INVALID", "PEER_ID_INVALID"], "messages.getStickerSet": ["STICKERSET_INVALID"], "messages.getUnreadMentions": ["PEER_ID_INVALID"], "messages.getWebPage": ["WC_CONVERT_URL_INVALID"], "messages.hideReportSpam": ["PEER_ID_INVALID"], "messages.importChatInvite": ["CHANNELS_TOO_MUCH", "INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID", "USER_ALREADY_PARTICIPANT", "USERS_TOO_MUCH"], "messages.installStickerSet": ["STICKERSET_INVALID"], "messages.migrateChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.readEncryptedHistory": ["MSG_WAIT_FAILED"], "messages.readHistory": ["PEER_ID_INVALID"], "messages.receivedQueue": ["MSG_WAIT_FAILED"], "messages.reorderPinnedDialogs": ["PEER_ID_INVALID"], "messages.reportEncryptedSpam": ["CHAT_ID_INVALID"], "messages.reportSpam": ["PEER_ID_INVALID"], "messages.requestEncryption": ["DH_G_A_INVALID", "USER_ID_INVALID"], "messages.saveDraft": ["PEER_ID_INVALID"], "messages.saveGif": ["GIF_ID_INVALID"], "messages.saveRecentSticker": ["STICKER_ID_INVALID"], "messages.search": ["CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "PEER_ID_NOT_SUPPORTED", "SEARCH_QUERY_EMPTY", "USER_ID_INVALID"], "messages.searchGifs": ["SEARCH_QUERY_EMPTY"], "messages.searchGlobal": ["SEARCH_QUERY_EMPTY"], "messages.sendEncrypted": ["CHAT_ID_INVALID", "DATA_INVALID", "ENCRYPTION_DECLINED", "MSG_WAIT_FAILED"], "messages.sendEncryptedFile": ["MSG_WAIT_FAILED"], "messages.sendEncryptedService": ["DATA_INVALID", "ENCRYPTION_DECLINED", "MSG_WAIT_FAILED"], "messages.sendInlineBotResult": ["INLINE_RESULT_EXPIRED", "PEER_ID_INVALID", "QUERY_ID_EMPTY", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "messages.sendMedia": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "EXTERNAL_URL_INVALID", "FILE_PART_122_MISSING", "FILE_PART_458_MISSING", "FILE_PART_468_MISSING", "FILE_PART_504_MISSING", "FILE_PART_72_MISSING", "FILE_PART_94_MISSING", "FILE_PART_LENGTH_INVALID", "FILE_PARTS_INVALID", "INPUT_USER_DEACTIVATED", "MEDIA_CAPTION_TOO_LONG", "MEDIA_EMPTY", "PEER_ID_INVALID", "PHOTO_EXT_INVALID", "PHOTO_INVALID_DIMENSIONS", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "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"], "messages.sendScreenshotNotification": ["PEER_ID_INVALID"], "messages.setBotCallbackAnswer": ["QUERY_ID_INVALID"], "messages.setBotPrecheckoutResults": ["ERROR_TEXT_EMPTY"], "messages.setBotShippingResults": ["QUERY_ID_INVALID"], "messages.setEncryptedTyping": ["CHAT_ID_INVALID"], "messages.setGameScore": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "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"], "messages.setInlineGameScore": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.setTyping": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT"], "messages.startBot": ["BOT_INVALID", "PEER_ID_INVALID", "START_PARAM_EMPTY", "START_PARAM_INVALID"], "messages.toggleChatAdmins": ["CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "messages.toggleDialogPin": ["PEER_ID_INVALID"], "messages.uninstallStickerSet": ["STICKERSET_INVALID"], "messages.uploadMedia": ["BOT_MISSING", "MEDIA_INVALID", "PEER_ID_INVALID"], "payments.getPaymentForm": ["MESSAGE_ID_INVALID"], "payments.getPaymentReceipt": ["MESSAGE_ID_INVALID"], "payments.sendPaymentForm": ["MESSAGE_ID_INVALID"], "payments.validateRequestedInfo": ["MESSAGE_ID_INVALID"], "phone.acceptCall": ["CALL_ALREADY_ACCEPTED", "CALL_ALREADY_DECLINED", "CALL_PEER_INVALID", "CALL_PROTOCOL_FLAGS_INVALID"], "phone.confirmCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.discardCall": ["CALL_ALREADY_ACCEPTED", "CALL_PEER_INVALID"], "phone.receivedCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.requestCall": ["CALL_PROTOCOL_FLAGS_INVALID", "PARTICIPANT_VERSION_OUTDATED", "USER_ID_INVALID"], "phone.saveCallDebug": ["CALL_PEER_INVALID", "DATA_JSON_INVALID"], "phone.setCallRating": ["CALL_PEER_INVALID"], "photos.getUserPhotos": ["MAX_ID_INVALID", "USER_ID_INVALID"], "photos.uploadProfilePhoto": ["FILE_PARTS_INVALID", "IMAGE_PROCESS_FAILED", "PHOTO_CROP_SIZE_SMALL", "PHOTO_EXT_INVALID"], "stickers.addStickerToSet": ["BOT_MISSING", "STICKERSET_INVALID"], "stickers.changeStickerPosition": ["BOT_MISSING", "STICKER_INVALID"], "stickers.createStickerSet": ["BOT_MISSING", "PACK_SHORT_NAME_INVALID", "PACK_SHORT_NAME_OCCUPIED", "PEER_ID_INVALID", "STICKER_EMOJI_INVALID", "STICKER_FILE_INVALID", "STICKER_PNG_DIMENSIONS", "STICKERS_EMPTY", "USER_ID_INVALID"], "stickers.removeStickerFromSet": ["BOT_MISSING", "STICKER_INVALID"], "updates.getChannelDifference": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID", "RANGES_INVALID"], "updates.getDifference": ["CDN_METHOD_INVALID", "DATE_EMPTY", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID"], "upload.getCdnFileHashes": ["CDN_METHOD_INVALID", "RSA_DECRYPT_FAILED"], "upload.getFile": ["FILE_ID_INVALID", "INPUT_FETCH_FAIL", "LIMIT_INVALID", "LOCATION_INVALID", "OFFSET_INVALID"], "upload.getWebFile": ["LOCATION_INVALID"], "upload.reuploadCdnFile": ["RSA_DECRYPT_FAILED"], "upload.saveBigFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "FILE_PART_SIZE_INVALID", "FILE_PARTS_INVALID"], "upload.saveFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "INPUT_FETCH_FAIL"], "users.getFullUser": ["USER_ID_INVALID"], "{}": ["INVITE_HASH_EXPIRED"]}, "401": {"account.updateStatus": ["SESSION_PASSWORD_NEEDED"], "auth.signIn": ["SESSION_PASSWORD_NEEDED"], "contacts.resolveUsername": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "messages.getDialogs": ["SESSION_PASSWORD_NEEDED"], "messages.getHistory": ["AUTH_KEY_PERM_EMPTY"], "messages.importChatInvite": ["SESSION_PASSWORD_NEEDED"], "updates.getDifference": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "updates.getState": ["SESSION_PASSWORD_NEEDED"], "upload.saveFilePart": ["SESSION_PASSWORD_NEEDED"], "users.getUsers": ["SESSION_PASSWORD_NEEDED"]}, "403": {"channels.createChannel": ["USER_RESTRICTED"], "channels.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "channels.editAdmin": ["CHAT_ADMIN_INVITE_REQUIRED", "RIGHT_FORBIDDEN", "USER_PRIVACY_RESTRICTED"], "channels.getFullChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "channels.inviteToChannel": ["CHAT_WRITE_FORBIDDEN", "USER_CHANNELS_TOO_MUCH", "USER_PRIVACY_RESTRICTED"], "channels.leaveChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "invokeWithLayer": ["CHAT_WRITE_FORBIDDEN"], "messages.addChatUser": ["USER_NOT_MUTUAL_CONTACT", "USER_PRIVACY_RESTRICTED"], "messages.createChat": ["USER_RESTRICTED"], "messages.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.editMessage": ["CHAT_WRITE_FORBIDDEN", "MESSAGE_AUTHOR_REQUIRED"], "messages.forwardMessages": ["CHAT_SEND_GIFS_FORBIDDEN", "CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_SEND_STICKERS_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "messages.getMessageEditData": ["MESSAGE_AUTHOR_REQUIRED"], "messages.migrateChat": ["CHAT_ADMIN_REQUIRED"], "messages.sendEncryptedService": ["USER_IS_BLOCKED"], "messages.sendInlineBotResult": ["CHAT_WRITE_FORBIDDEN"], "messages.sendMedia": ["CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "messages.sendMessage": ["CHAT_WRITE_FORBIDDEN"], "messages.setInlineBotResults": ["USER_BOT_INVALID"], "messages.setTyping": ["CHAT_WRITE_FORBIDDEN"], "phone.requestCall": ["USER_IS_BLOCKED", "USER_PRIVACY_RESTRICTED"], "updates.getChannelDifference": ["CHANNEL_PUBLIC_GROUP_NA"]}, "500": {"auth.sendCode": ["AUTH_RESTART"], "auth.signUp": ["MEMBER_OCCUPY_PRIMARY_LOC_FAILED", "REG_ID_GENERATE_FAILED"], "channels.getChannels": ["NEED_CHAT_INVALID"], "contacts.deleteContacts": ["NEED_MEMBER_INVALID"], "contacts.importCard": ["NEED_MEMBER_INVALID"], "invokeWithLayer": ["NEED_MEMBER_INVALID"], "messages.acceptEncryption": ["ENCRYPTION_OCCUPY_FAILED"], "messages.editChatTitle": ["NEED_CHAT_INVALID"], "messages.forwardMessages": ["PTS_CHANGE_EMPTY", "RANDOM_ID_DUPLICATE"], "messages.sendMedia": ["RANDOM_ID_DUPLICATE", "STORAGE_CHECK_FAILED"], "messages.sendMessage": ["RANDOM_ID_DUPLICATE"], "phone.acceptCall": ["CALL_OCCUPY_FAILED"], "phone.requestCall": ["PARTICIPANT_CALL_FAILED"], "updates.getChannelDifference": ["HISTORY_GET_FAILED", "PERSISTENT_TIMESTAMP_OUTDATED"], "updates.getDifference": ["NEED_MEMBER_INVALID", "STORE_INVALID_SCALAR_TYPE"], "upload.getCdnFile": ["UNKNOWN_METHOD"], "users.getUsers": ["MEMBER_NO_LOCATION", "NEED_MEMBER_INVALID"]}}} \ No newline at end of file From a7888bfaf8f5e5c602619ce02bcfc24edbc5d209 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 30 Jan 2018 09:11:40 +0100 Subject: [PATCH 133/361] Fix tiny typo on the documentation --- readthedocs/extra/advanced-usage/sessions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index 7f1ded9b..ae2b03ad 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -4,7 +4,7 @@ Session Files ============== -The first parameter you pass the the constructor of the ``TelegramClient`` is +The first parameter you pass to the constructor of the ``TelegramClient`` is the ``session``, and defaults to be the session name (or full path). That is, if you create a ``TelegramClient('anon')`` instance and connect, an ``anon.session`` file will be created on the working directory. From bf56d3211828f34b024c7660accd963d933a369a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 30 Jan 2018 18:32:42 +0100 Subject: [PATCH 134/361] Add missing FutureSalts response special case (#81) --- telethon/network/mtproto_sender.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 0e960181..877611df 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -4,11 +4,9 @@ encrypting every packet, and relies on a valid AuthKey in the used Session. """ import gzip import logging -import struct from threading import Lock from .. import helpers as utils -from ..crypto import AES from ..errors import ( BadMessageError, InvalidChecksumError, BrokenAuthKeyError, rpc_message_to_error @@ -16,11 +14,11 @@ from ..errors import ( from ..extensions import BinaryReader from ..tl import TLMessage, MessageContainer, GzipPacked from ..tl.all_tlobjects import tlobjects +from ..tl.functions.auth import LogOutRequest from ..tl.types import ( - MsgsAck, Pong, BadServerSalt, BadMsgNotification, + MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts, MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo ) -from ..tl.functions.auth import LogOutRequest __log__ = logging.getLogger(__name__) @@ -244,6 +242,12 @@ class MtProtoSender: return True + if isinstance(obj, FutureSalts): + r = self._pop_request(obj.req_msg_id) + if r: + r.result = obj + r.confirm_received.set() + # If the object isn't any of the above, then it should be an Update. self.session.process_entities(obj) if state: From c8bbbe3e3ce2b9db683ce3085fca19841c26562a Mon Sep 17 00:00:00 2001 From: Birger Jarl Date: Wed, 31 Jan 2018 23:01:53 +0300 Subject: [PATCH 135/361] Save session data when migrating from JSON (#570) --- telethon/session.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/session.py b/telethon/session.py index 266d732e..e168e559 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -159,6 +159,7 @@ class Session: 'insert or replace into entities values (?,?,?,?,?)', entities ) + self._update_session_table() c.close() self.save() From d5a91c727332b5c9c4466f4c6d55f67029d0d2d0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Feb 2018 09:39:00 +0100 Subject: [PATCH 136/361] Don't set session to None on .log_out() --- telethon/telegram_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 18602032..4ae7f642 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -418,7 +418,6 @@ class TelegramClient(TelegramBareClient): self.disconnect() self.session.delete() - self.session = None return True def get_me(self): From add122bfe7b376d0a4ad27786b6cd87186e17e1e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Feb 2018 10:12:46 +0100 Subject: [PATCH 137/361] Support signing up through .start() --- telethon/telegram_client.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 4ae7f642..84c77b9c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -27,7 +27,8 @@ from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, - SessionPasswordNeededError, FileMigrateError + SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError, + PhoneNumberOccupiedError ) from .network import ConnectionMode from .tl.custom import Draft, Dialog @@ -204,7 +205,8 @@ class TelegramClient(TelegramBareClient): def start(self, phone=lambda: input('Please enter your phone: '), password=None, bot_token=None, - force_sms=False, code_callback=None): + force_sms=False, code_callback=None, + first_name='New User', last_name=''): """ Convenience method to interactively connect and sign in if required, also taking into consideration that 2FA may be enabled in the account. @@ -236,6 +238,13 @@ class TelegramClient(TelegramBareClient): A callable that will be used to retrieve the Telegram login code. Defaults to `input()`. + first_name (:obj:`str`, optional): + The first name to be used if signing up. This has no + effect if the account already exists and you sign in. + + last_name (:obj:`str`, optional): + Similar to the first name, but for the last. Optional. + Returns: :obj:`TelegramClient`: This client, so initialization can be chained with `.start()`. @@ -276,19 +285,28 @@ class TelegramClient(TelegramBareClient): max_attempts = 3 two_step_detected = False - self.send_code_request(phone, force_sms=force_sms) + sent_code = self.send_code_request(phone, force_sms=force_sms) + sign_up = not sent_code.phone_registered while attempts < max_attempts: try: - # Raises SessionPasswordNeededError if 2FA enabled - me = self.sign_in(phone, code_callback()) + if sign_up: + me = self.sign_up(code_callback(), first_name, last_name) + else: + # Raises SessionPasswordNeededError if 2FA enabled + me = self.sign_in(phone, code_callback()) break except SessionPasswordNeededError: two_step_detected = True break + except PhoneNumberOccupiedError: + sign_up = False + except PhoneNumberUnoccupiedError: + sign_up = True except (PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError): print('Invalid code. Please try again.', file=sys.stderr) - attempts += 1 + + attempts += 1 else: raise RuntimeError( '{} consecutive sign-in attempts failed. Aborting' From fbd53e2126f8f46a86ed3bee824817a3dad793af Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Feb 2018 12:10:03 +0100 Subject: [PATCH 138/361] Override TLObject's __eq__ and __ne__ methods --- telethon/tl/tlobject.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index db1982c4..b048158c 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -144,6 +144,12 @@ class TLObject: def on_response(self, reader): self.result = reader.tgread_object() + def __eq__(self, o): + return isinstance(o, type(self)) and self.to_dict() == o.to_dict() + + def __ne__(self, o): + return not isinstance(o, type(self)) or self.to_dict() != o.to_dict() + def __str__(self): return TLObject.pretty_format(self) From cf21808118737005beb3450e31a387b6c41b285f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 2 Feb 2018 17:23:28 +0100 Subject: [PATCH 139/361] Raise error on .get_entity() on non-joined invite link --- telethon/telegram_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 84c77b9c..9dda06af 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1716,10 +1716,10 @@ class TelegramClient(TelegramBareClient): if is_join_chat: invite = self(CheckChatInviteRequest(string)) if isinstance(invite, ChatInvite): - # If it's an invite to a chat, the user must join before - # for the link to be resolved and work, otherwise raise. - if invite.channel: - return invite.channel + raise ValueError( + 'Cannot get entity from a channel ' + '(or group) that you are not part of' + ) elif isinstance(invite, ChatInviteAlready): return invite.chat else: From 2ffe2b71dc019792c4b34c5ed070e9299021a461 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 11:39:15 +0100 Subject: [PATCH 140/361] Except OSError with errno.WSAEACCES when connecting "OSError: [WinError 10013] An attempt was made to access a socket in a way forbidden by its access permissions." --- telethon/extensions/tcp_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index d4c45776..a306302a 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -73,7 +73,8 @@ class TcpClient: # There are some errors that we know how to handle, and # the loop will allow us to retry if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL, - errno.ECONNREFUSED): + errno.ECONNREFUSED, # Windows-specific follow + getattr(errno, 'WSAEACCES', None)): # Bad file descriptor, i.e. socket was closed, set it # to none to recreate it on the next iteration self._socket = None From eefd37c2d78a48429f6fcceb3cfb4dbbc7affe5f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 12:15:38 +0100 Subject: [PATCH 141/361] Stop calling .disconnect() from .__del__() It was causing some strange behaviour with the synchronized Queue used by the UpdateState class. Calling .get() with any timeout would block forever. Perhaps something else got released when the script ended and then any call would block forever, thus the thread never joining. --- telethon/telegram_bare_client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index fe63ab8a..bff6c1d3 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -255,9 +255,6 @@ class TelegramBareClient: self._first_request = True # On reconnect it will be first again self.session.close() - def __del__(self): - self.disconnect() - def _reconnect(self, new_dc=None): """If 'new_dc' is not set, only a call to .connect() will be made since it's assumed that the connection has been lost and the From 341fb38136418c9ba616b70103618be390d9d1cb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 15:39:37 +0100 Subject: [PATCH 142/361] Invoke getState after the server kicks us idling for updates For some reason, the server seems to kick us after 1024 items from the network are received. Tested with the following code, 1022 updates were received, after BadServerSalt, NewSessionCreated and MsgsAck: client = TelegramClient(..., spawn_read_thread=False) client.connect(_sync_updates=False) sender = client._sender client = None while True: try: sender.receive(None) except TimeoutError: pass except ConnectionResetError: sender.connect() If one were to run this code after being kicked no further items will be retrieved and it will always timeout. Invoking a ping has no effect either. Invoking some "high level" request like getState seems to do the trick. --- telethon/telegram_bare_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index bff6c1d3..5984bb2e 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -664,6 +664,14 @@ class TelegramBareClient: with self._reconnect_lock: while self._user_connected and not self._reconnect(): sleep(0.1) # Retry forever, this is instant messaging + + if self.is_connected(): + # Telegram seems to kick us every 1024 items received + # from the network not considering things like bad salt. + # We must execute some *high level* request (that's not + # a ping) if we want to receive updates again. + # TODO Test if getDifference works too (better alternative) + self._sender.send(GetStateRequest()) except: self._idling.clear() raise From fd08d5325393e1f650e682ef3cb9427507ea0d2b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 15:42:43 +0100 Subject: [PATCH 143/361] Trust the server will not send duplicates This change was also suggested by the test on the previous commit. --- telethon/update_state.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index 9f308d89..f98c0c04 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -30,7 +30,6 @@ class UpdateState: self.handlers = [] self._updates_lock = RLock() self._updates = Queue() - self._latest_updates = deque(maxlen=10) # https://core.telegram.org/api/updates self._state = tl.updates.State(0, 0, datetime.now(), 0, 0) @@ -130,34 +129,12 @@ class UpdateState: self._state = update return # Nothing else to be done - pts = getattr(update, 'pts', self._state.pts) - if hasattr(update, 'pts') and pts <= self._state.pts: - __log__.info('Ignoring %s, already have it', update) - return # We already handled this update - - 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: - __log__.info('Ignoring %s, already have it', update) - return # Duplicated too - - self._latest_updates.append(data) + if hasattr(update, 'pts'): + self._state.pts = update.pts + # After running the script for over an hour and receiving over + # 1000 updates, the only duplicates received were users going + # online or offline. We can trust the server until new reports. if isinstance(update, tl.UpdateShort): self._updates.put(update.update) # Expand "Updates" into "Update", and pass these to callbacks. From 06bc761a5b794283c895e66460edd7bdeee187d7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 16:03:17 +0100 Subject: [PATCH 144/361] Update to v0.17 --- readthedocs/extra/changelog.rst | 45 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 580ebe4b..2e11fc7d 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,51 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Trust the Server with Updates (v0.17) +===================================== + +*Published at 2018/02/03* + +The library trusts the server with updates again. The library will *not* +check for duplicates anymore, and when the server kicks us, it will run +``GetStateRequest`` so the server starts sending updates again (something +it wouldn't do unless you invoked something, it seems). But this update +also brings a few more changes! + +Additions +~~~~~~~~~ + +- ``TLObject``'s override ``__eq__`` and ``__ne__``, so you can compare them. +- Added some missing cases on ``.get_input_entity()`` and peer functions. +- ``obj.to_dict()`` now has a ``'_'`` key with the type used. +- ``.start()`` can also sign up now. +- More parameters for ``.get_message_history()``. +- Updated list of RPC errors. +- HTML parsing thanks to **@tulir**! It can be used similar to markdown: + ``client.send_message(..., parse_mode='html')``. + + +Enhancements +~~~~~~~~~~~~ + +- ``client.send_file()`` now accepts ``Message``'s and + ``MessageMedia``'s as the ``file`` parameter. +- Some documentation updates and fixed to clarify certain things. +- New exact match feature on https://lonamiwebs.github.io/Telethon. +- Return as early as possible from ``.get_input_entity()`` and similar, + to avoid penalizing you for doing this right. + +Bug fixes +~~~~~~~~~ + +- ``.download_media()`` wouldn't accept a ``Document`` as parameter. +- The SQLite is now closed properly on disconnection. +- IPv6 addresses shouldn't use square braces. +- Fix regarding ``.log_out()``. +- The time offset wasn't being used (so having wrong system time would + cause the library not to work at all). + + New ``.resolve()`` method (v0.16.2) =================================== diff --git a/telethon/version.py b/telethon/version.py index 28c39d24..675ca0af 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.16.2' +__version__ = '0.17' From f200369a9327f71c8c0260c07ebdaf66d8057526 Mon Sep 17 00:00:00 2001 From: tsujp Date: Tue, 6 Feb 2018 19:21:09 +0900 Subject: [PATCH 145/361] Add Heroku instructions to sessions documentation (#586) --- readthedocs/extra/advanced-usage/.DS_Store | Bin 0 -> 6148 bytes readthedocs/extra/advanced-usage/sessions.rst | 49 ++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 readthedocs/extra/advanced-usage/.DS_Store diff --git a/readthedocs/extra/advanced-usage/.DS_Store b/readthedocs/extra/advanced-usage/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0= 3.8.2). Heroku uses +SQLite 3.7.9 which does not support ``WITHOUT ROWID``. So, if you generated +your session file on a system with SQLite >= 3.8.2 your session file will not +work on Heroku's platform and will throw a corrupted schema error. + +There are multiple ways to solve this, the easiest of which is generating a +session file on your Heroku dyno itself. The most complicated is creating +a custom buildpack to install SQLite >= 3.8.2. + + +Generating Session File on a Heroku Dyno +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + Do not restart your application Dyno at any point prior to retrieving your + session file. Constantly creating new session files from Telegram's API + will result in a 24 hour rate limit ban. + +Due to Heroku's ephemeral filesystem all dynamically generated +files not part of your applications buildpack or codebase are destroyed upon +each restart. + +Using this scaffolded code we can start the authentication process: + + .. code-block:: python + + client = TelegramClient('login.session', api_id, api_hash).start() + +At this point your Dyno will crash because you cannot access stdin. Open your +Dyno's control panel on the Heroku website and "run console" from the "More" +dropdown at the top right. Enter ``bash`` and wait for it to load. + +You will automatically be placed into your applications working directory. +So run your application ``python app.py`` and now you can complete the input +requests such as "what is your phone number" etc. + +Once you're successfully authenticated exit your application script with +CTRL + C and ``ls`` to confirm ``login.session`` exists in your current +directory. Now you can create a git repo on your account and commit +``login.session`` to that repo. + +You cannot ``ssh`` into your Dyno instance because it has crashed, so unless +you programatically upload this file to a server host this is the only way to +get it off of your Dyno. From 4362c02e9293ef7d1f8430c0c6c12f66257fc301 Mon Sep 17 00:00:00 2001 From: tsujp Date: Tue, 6 Feb 2018 20:13:38 +0900 Subject: [PATCH 146/361] Add further Heroku instructions to session documentation (#588) --- readthedocs/extra/advanced-usage/sessions.rst | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index 765641ab..fca7828e 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -59,8 +59,13 @@ session file on your Heroku dyno itself. The most complicated is creating a custom buildpack to install SQLite >= 3.8.2. -Generating Session File on a Heroku Dyno -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Generating a Session File on a Heroku Dyno +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + Due to Heroku's ephemeral filesystem all dynamically generated + files not part of your applications buildpack or codebase are destroyed + upon each restart. .. warning:: Do not restart your application Dyno at any point prior to retrieving your @@ -78,7 +83,7 @@ Using this scaffolded code we can start the authentication process: client = TelegramClient('login.session', api_id, api_hash).start() At this point your Dyno will crash because you cannot access stdin. Open your -Dyno's control panel on the Heroku website and "run console" from the "More" +Dyno's control panel on the Heroku website and "Run console" from the "More" dropdown at the top right. Enter ``bash`` and wait for it to load. You will automatically be placed into your applications working directory. @@ -93,3 +98,16 @@ directory. Now you can create a git repo on your account and commit You cannot ``ssh`` into your Dyno instance because it has crashed, so unless you programatically upload this file to a server host this is the only way to get it off of your Dyno. + +You now have a session file compatible with SQLite <= 3.8.2. Now you can +programatically fetch this file from an external host (Firebase, S3 etc.) +and login to your session using the following scaffolded code: + + .. code-block:: python + + fileName, headers = urllib.request.urlretrieve(file_url, 'login.session') + client = TelegramClient(os.path.abspath(fileName), api_id, api_hash).start() + +.. note:: + - ``urlretrieve`` will be depreciated, consider using ``requests``. + - ``file_url`` represents the location of your file. From 5ec984dd820a00d005306b6144b86b9b54346c99 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 10:41:58 +0100 Subject: [PATCH 147/361] Allow adding events with the client.on decorator --- telethon/events/__init__.py | 14 ++++++++++++++ telethon/telegram_client.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 telethon/events/__init__.py diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py new file mode 100644 index 00000000..d4d1e019 --- /dev/null +++ b/telethon/events/__init__.py @@ -0,0 +1,14 @@ +import abc +from ..tl import types, functions +from ..extensions import markdown +from .. import utils + + +class _EventBuilder(abc.ABC): + @abc.abstractmethod + def build(self, update): + """Builds an event for the given update if possible, or returns None""" + + @abc.abstractmethod + def resolve(self, client): + """Helper method to allow event builders to be resolved before usage""" diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9dda06af..c4f5d722 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -160,6 +160,8 @@ class TelegramClient(TelegramBareClient): **kwargs ) + self._event_builders = [] + # Some fields to easy signing in. Let {phone: hash} be # a dictionary because the user may change their mind. self._phone_code_hash = {} @@ -1623,6 +1625,41 @@ class TelegramClient(TelegramBareClient): # endregion + # region Event handling + + def on(self, event): + """ + + Turns the given entity into a valid Telegram user or chat. + + Args: + event (:obj:`_EventBuilder` | :obj:`type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + """ + if isinstance(event, type): + event = event() + + event.resolve(self) + + def decorator(f): + self._event_builders.append((event, f)) + return f + + if self._on_handler not in self.updates.handlers: + self.add_update_handler(self._on_handler) + + return decorator + + def _on_handler(self, update): + for builder, callback in self._event_builders: + event = builder.build(update) + if event: + event._client = self + callback(event) + + # endregion + # region Small utilities to make users' life easier def get_entity(self, entity): From ef837b1a5325b080b4e282fa3a1aa3bc358ab565 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 10:42:40 +0100 Subject: [PATCH 148/361] Add a NewMessage event to handle incoming messages --- telethon/events/__init__.py | 240 ++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d4d1e019..683945a7 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -12,3 +12,243 @@ class _EventBuilder(abc.ABC): @abc.abstractmethod def resolve(self, client): """Helper method to allow event builders to be resolved before usage""" + + +# Classes defined here are actually Event builders +# for their inner Event classes. Inner ._client is +# set later by the creator TelegramClient. +class NewMessage(_EventBuilder): + """ + Represents a new message event builder. + + Args: + incoming (:obj:`bool`, optional): + If set to ``True``, only **incoming** messages will be handled. + Mutually exclusive with ``outgoing`` (can only set one of either). + + outgoing (:obj:`bool`, optional): + If set to ``True``, only **outgoing** messages will be handled. + Mutually exclusive with ``incoming`` (can only set one of either). + + chats (:obj:`entity`, optional): + May be one or more entities (username/peer/etc.). By default, + only matching chats will be handled. + + blacklist_chats (:obj:`bool`, optional): + Whether to treat the the list of chats as a blacklist (if + it matches it will NOT be handled) or a whitelist (default). + """ + def __init__(self, incoming=None, outgoing=None, + chats=None, blacklist_chats=False, + require_input=True): + if incoming and outgoing: + raise ValueError('Can only set either incoming or outgoing') + + self.incoming = incoming + self.outgoing = outgoing + self.chats = chats + self.blacklist_chats = blacklist_chats + + def resolve(self, client): + if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): + self.chats = set(utils.get_peer_id(x) + for x in client.get_input_entity(self.chats)) + elif self.chats is not None: + self.chats = {utils.get_peer_id( + client.get_input_entity(self.chats))} + + def build(self, update): + if isinstance(update, + (types.UpdateNewMessage, types.UpdateNewChannelMessage)): + event = NewMessage.Event(update.message) + elif isinstance(update, types.UpdateShortMessage): + event = NewMessage.Event(types.Message( + out=update.out, + mentioned=update.mentioned, + media_unread=update.media_unread, + silent=update.silent, + id=update.id, + to_id=types.PeerUser(update.user_id), + message=update.message, + date=update.date, + fwd_from=update.fwd_from, + via_bot_id=update.via_bot_id, + reply_to_msg_id=update.reply_to_msg_id, + entities=update.entities + )) + else: + return + + # Short-circuit if we let pass all events + if all(x is None for x in (self.incoming, self.outgoing, self.chats)): + return event + + if self.incoming and event.message.out: + return + if self.outgoing and not event.message.out: + return + + if self.chats is not None: + inside = utils.get_peer_id(event.input_chat) in self.chats + if inside == self.blacklist_chats: + # If this chat matches but it's a blacklist ignore. + # If it doesn't match but it's a whitelist ignore. + return + + # Tests passed so return the event + return event + + class Event: + """ + Represents the event of a new message. + + Members: + message (:obj:`Message`): + This is the original ``Message`` object. + + input_chat (:obj:`InputPeer`): + This is the input chat (private, group, megagroup or channel) + to which the message was sent. This doesn't have the title or + anything, but is useful if you don't need those to avoid + further requests. + + Note that this might not be available if the library can't + find the input chat. + + chat (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional): + This property will make an API call the first time to get the + most up to date version of the chat, so use with care as + there is no caching besides local caching yet. + + ``input_chat`` needs to be available (often the case). + + is_private (:obj:`bool`): + True if the message was sent as a private message. + + is_group (:obj:`bool`): + True if the message was sent on a group or megagroup. + + is_channel (:obj:`bool`): + True if the message was sent on a megagroup or channel. + + input_sender (:obj:`InputPeer`): + This is the input version of the user who sent the message. + Similarly to ``input_chat``, this doesn't have things like + username or similar, but still useful in some cases. + + Note that this might not be available if the library can't + find the input chat. + + sender (:obj:`User`): + This property will make an API call the first time to get the + most up to date version of the sender, so use with care as + there is no caching besides local caching yet. + + ``input_sender`` needs to be available (often the case). + + text (:obj:`str`): + The message text, markdown-formatted. + + raw_text (:obj:`str`): + The raw message text, ignoring any formatting. + + is_reply (:obj:`str`): + Whether the message is a reply to some other or not. + + reply_message (:obj:`Message`, optional): + This property will make an API call the first time to get the + full ``Message`` object that one was replying to, so use with + care as there is no caching besides local caching yet. + + forward (:obj:`MessageFwdHeader`, optional): + The unmodified ``MessageFwdHeader``, if present. + + out (:obj:`bool`): + Whether the message is outgoing (i.e. you sent it from + another session) or incoming (i.e. someone else sent it). + """ + def __init__(self, message): + self._client = None + self.message = message + self._text = None + + self._chat = None + self._sender = None + + self.is_private = isinstance(message.to_id, types.PeerUser) + self.is_group = ( + isinstance(message.to_id, (types.PeerChat, types.PeerChannel)) + and not message.post + ) + self.is_channel = isinstance(message.to_id, types.PeerChannel) + + self.is_reply = bool(message.reply_to_msg_id) + self._reply_message = None + + def reply(self, message, as_reply=True): + """Replies to this message""" + self._client.send_message(self.message.to_id, message) + + @property + def input_chat(self): + # TODO If not found, getMessages to find the sender and chat + return self._client.get_input_entity(self.message.to_id) + + @property + def chat(self): + if self._chat is None: + # TODO Assert input entity is not None to avoid weird errors + self._chat = self._client.get_entity(self.input_chat) + return self._chat + + @property + def input_sender(self): + # TODO If not found, getMessages to find the sender and chat + return self._client.get_input_entity(self.message.from_id) + + @property + def sender(self): + if self._sender is None: + # TODO Assert input entity is not None to avoid weird errors + self._sender = self._client.get_entity(self.input_sender) + return self._sender + + @property + def text(self): + if self._text is None: + if not self.message.entities: + return self.message.message + self._text = markdown.unparse(self.message.message, + self.message.entities or []) + return self._text + + @property + def raw_text(self): + return self.message.message + + @property + def reply_message(self): + if not self.message.reply_to_msg_id: + return None + + if self._reply_message is None: + if isinstance(self.input_chat, types.InputPeerChannel): + r = self._client(functions.channels.GetMessagesRequest( + self.input_chat, [self.message.reply_to_msg_id] + )) + else: + r = self._client(functions.messages.GetMessagesRequest( + [self.message.reply_to_msg_id] + )) + if not isinstance(r, types.messages.MessagesNotModified): + self._reply_message = r.messages[0] + + return self._reply_message + + @property + def forward(self): + return self.message.fwd_from + + @property + def out(self): + return self.message.out From 9c09233b4fd0b207802622656f6c055d7333c81a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 13:45:17 +0100 Subject: [PATCH 149/361] Make NewMessage's input chat/sender actual Input* if possible --- telethon/events/__init__.py | 75 +++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 683945a7..d7012ad9 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,7 +1,10 @@ import abc -from ..tl import types, functions -from ..extensions import markdown +import itertools + from .. import utils +from ..errors import RPCError +from ..extensions import markdown +from ..tl import types, functions class _EventBuilder(abc.ABC): @@ -172,7 +175,9 @@ class NewMessage(_EventBuilder): self.message = message self._text = None + self._input_chat = None self._chat = None + self._input_sender = None self._sender = None self.is_private = isinstance(message.to_id, types.PeerUser) @@ -189,28 +194,76 @@ class NewMessage(_EventBuilder): """Replies to this message""" self._client.send_message(self.message.to_id, message) + def _get_input_entity(self, msg_id, entity_id, chat=None): + """ + Helper function to call GetMessages on the give msg_id and + return the input entity whose ID is the given entity ID. + """ + try: + if isinstance(chat, types.InputPeerChannel): + result = self._client( + functions.channels.GetMessagesRequest(chat, [msg_id]) + ) + else: + result = self._client( + functions.messages.GetMessagesRequest([msg_id]) + ) + except RPCError: + return + entity = { + utils.get_peer_id(x): x for x in itertools.chain( + getattr(result, 'chats', []), + getattr(result, 'users', [])) + }.get(entity_id) + if entity: + return utils.get_input_peer(entity) + @property def input_chat(self): - # TODO If not found, getMessages to find the sender and chat - return self._client.get_input_entity(self.message.to_id) + if self._input_chat is None: + try: + self._input_chat = self._client.get_input_entity( + self.message.to_id + ) + except (ValueError, TypeError): + # The library hasn't seen this chat, get the message + if not isinstance(self.message.to_id, types.PeerChannel): + # TODO For channels, getDifference? Maybe looking + # in the dialogs (which is already done) is enough. + self._input_chat = self._get_input_entity( + self.message.id, + utils.get_peer_id(self.message.to_id) + ) + return self._input_chat @property def chat(self): - if self._chat is None: - # TODO Assert input entity is not None to avoid weird errors - self._chat = self._client.get_entity(self.input_chat) + if self._chat is None and self.input_chat: + self._chat = self._client.get_entity(self._input_chat) return self._chat @property def input_sender(self): - # TODO If not found, getMessages to find the sender and chat + if self._input_sender is None: + try: + self._input_sender = self._client.get_input_entity( + self.message.from_id + ) + except (ValueError, TypeError): + if isinstance(self.message.to_id, types.PeerChannel): + # We can rely on self.input_chat for this + self._input_sender = self._get_input_entity( + self.message.id, + self.message.from_id, + chat=self.input_chat + ) + return self._client.get_input_entity(self.message.from_id) @property def sender(self): - if self._sender is None: - # TODO Assert input entity is not None to avoid weird errors - self._sender = self._client.get_entity(self.input_sender) + if self._sender is None and self.input_sender: + self._sender = self._client.get_entity(self._input_sender) return self._sender @property From dc43757cff0e9ce61b7fec2a4123ada61d0e4d5d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 13:55:25 +0100 Subject: [PATCH 150/361] Don't access NewMessage properties when building the event --- telethon/events/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d7012ad9..b5d7ae68 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -92,7 +92,7 @@ class NewMessage(_EventBuilder): return if self.chats is not None: - inside = utils.get_peer_id(event.input_chat) in self.chats + inside = utils.get_peer_id(event.message.to_id) in self.chats if inside == self.blacklist_chats: # If this chat matches but it's a blacklist ignore. # If it doesn't match but it's a whitelist ignore. From 2e0a8d6bce604c7949b1a1dd189edd4759cea4a4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 13:55:41 +0100 Subject: [PATCH 151/361] Add respond and reply methods to the NewMessage event --- telethon/events/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index b5d7ae68..4a6ae07f 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -190,9 +190,21 @@ class NewMessage(_EventBuilder): self.is_reply = bool(message.reply_to_msg_id) self._reply_message = None - def reply(self, message, as_reply=True): - """Replies to this message""" - self._client.send_message(self.message.to_id, message) + def respond(self, *args, **kwargs): + """ + Responds to the message (not as a reply). This is a shorthand for + ``client.send_message(event.chat, ...)``. + """ + return self._client.send_message(self.input_chat, *args, **kwargs) + + def reply(self, *args, **kwargs): + """ + Replies to the message (as a reply). This is a shorthand for + ``client.send_message(event.chat, ..., reply_to=event.message.id)``. + """ + return self._client.send_message(self.input_chat, + reply_to=self.message.id, + *args, **kwargs) def _get_input_entity(self, msg_id, entity_id, chat=None): """ From c79fbe451f70dbe83d61dbb5abcdd75dc670c69e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 14:06:36 +0100 Subject: [PATCH 152/361] Fix NewMessage event not dropping MessageService --- telethon/events/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 4a6ae07f..5fdd257d 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -63,6 +63,8 @@ class NewMessage(_EventBuilder): def build(self, update): if isinstance(update, (types.UpdateNewMessage, types.UpdateNewChannelMessage)): + if not isinstance(update.message, types.Message): + return # We don't care about MessageService's here event = NewMessage.Event(update.message) elif isinstance(update, types.UpdateShortMessage): event = NewMessage.Event(types.Message( From 91ba50174a52d80ca62ca1e981612e929845d3b0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Feb 2018 19:43:15 +0100 Subject: [PATCH 153/361] Provide easier access to media through NewMessage event --- telethon/events/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 5fdd257d..1477cc02 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -316,6 +316,24 @@ class NewMessage(_EventBuilder): def forward(self): return self.message.fwd_from + @property + def media(self): + return self.message.media + + @property + def photo(self): + if isinstance(self.message.media, types.MessageMediaPhoto): + photo = self.message.media.photo + if isinstance(photo, types.Photo): + return photo + + @property + def document(self): + if isinstance(self.message.media, types.MessageMediaDocument): + doc = self.message.media.document + if isinstance(doc, types.Document): + return doc + @property def out(self): return self.message.out From e15dd05975975139fee4ecb87e6c9525cde41e63 Mon Sep 17 00:00:00 2001 From: Kyle2142 Date: Fri, 9 Feb 2018 10:07:25 +0200 Subject: [PATCH 154/361] Corrected info in Admin Permissions example (#589) --- .../extra/examples/chats-and-channels.rst | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 30b94178..44ee6112 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -169,14 +169,28 @@ Giving or revoking admin permissions can be done with the `EditAdminRequest`__: pin_messages=True, invite_link=None, edit_messages=None - ) + ) + # Equivalent to: + # rights = ChannelAdminRights( + # change_info=True, + # delete_messages=True, + # pin_messages=True + # ) - client(EditAdminRequest(channel, who, rights)) + # Once you have a ChannelAdminRights, invoke it + client(EditAdminRequest(channel, user, rights)) - -Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set -to ``True`` the ``post_messages`` and ``edit_messages`` fields. Those that -are ``None`` can be omitted (left here so you know `which are available`__. + # User will now be able to change group info, delete other people's + # messages and pin messages. + +| Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all +| parameters to ``True`` to give a user full permissions, as not all +| permissions are related to both broadcast channels/megagroups. +| +| E.g. trying to set ``post_messages=True`` in a megagroup will raise an +| error. It is recommended to always use keyword arguments, and to set only +| the permissions the user needs. If you don't need to change a permission, +| it can be omitted (full list `here`__). __ https://lonamiwebs.github.io/Telethon/methods/channels/edit_admin.html __ https://github.com/Kyle2142 From 510bbf0fc8a5988378321f8fb10ad0548c06ff1e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 11:36:41 +0100 Subject: [PATCH 155/361] Create a more reusable Event base class --- telethon/events/__init__.py | 148 ++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 59 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 1477cc02..d16a6529 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -17,6 +17,90 @@ class _EventBuilder(abc.ABC): """Helper method to allow event builders to be resolved before usage""" +class _EventCommon(abc.ABC): + """Intermediate class with common things to all events""" + + def __init__(self, chat_peer=None, msg_id=None, broadcast=False): + self._client = None + self._chat_peer = chat_peer + self._message_id = msg_id + self._input_chat = None + self._chat = None + + self.is_private = isinstance(chat_peer, types.PeerUser) + self.is_group = ( + isinstance(chat_peer, (types.PeerChat, types.PeerChannel)) + and not broadcast + ) + self.is_channel = isinstance(chat_peer, types.PeerChannel) + + def _get_input_entity(self, msg_id, entity_id, chat=None): + """ + Helper function to call GetMessages on the give msg_id and + return the input entity whose ID is the given entity ID. + + If ``chat`` is present it must be an InputPeer. + """ + try: + if isinstance(chat, types.InputPeerChannel): + result = self._client( + functions.channels.GetMessagesRequest(chat, [msg_id]) + ) + else: + result = self._client( + functions.messages.GetMessagesRequest([msg_id]) + ) + except RPCError: + return + entity = { + utils.get_peer_id(x): x for x in itertools.chain( + getattr(result, 'chats', []), + getattr(result, 'users', [])) + }.get(entity_id) + if entity: + return utils.get_input_peer(entity) + + @property + def input_chat(self): + """ + The (:obj:`InputPeer`) (group, megagroup or channel) on which + the event occurred. This doesn't have the title or anything, + but is useful if you don't need those to avoid further + requests. + + Note that this might be ``None`` if the library can't find it. + """ + + if self._input_chat is None and self._chat_peer is not None: + try: + self._input_chat = self._client.get_input_entity( + self._chat_peer + ) + except (ValueError, TypeError): + # The library hasn't seen this chat, get the message + if not isinstance(self._chat_peer, types.PeerChannel): + # TODO For channels, getDifference? Maybe looking + # in the dialogs (which is already done) is enough. + if self._message_id is not None: + self._input_chat = self._get_input_entity( + self._message_id, + utils.get_peer_id(self._chat_peer) + ) + return self._input_chat + + @property + def chat(self): + """ + The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which + the event occurred. This property will make an API call the first time + to get the most up to date version of the chat, so use with care as + there is no caching besides local caching yet. + """ + if self._chat is None and self.input_chat: + self._chat = self._client.get_entity(self._input_chat) + return self._chat + + # Classes defined here are actually Event builders # for their inner Event classes. Inner ._client is # set later by the creator TelegramClient. @@ -42,8 +126,7 @@ class NewMessage(_EventBuilder): it matches it will NOT be handled) or a whitelist (default). """ def __init__(self, incoming=None, outgoing=None, - chats=None, blacklist_chats=False, - require_input=True): + chats=None, blacklist_chats=False): if incoming and outgoing: raise ValueError('Can only set either incoming or outgoing') @@ -103,7 +186,7 @@ class NewMessage(_EventBuilder): # Tests passed so return the event return event - class Event: + class Event(_EventCommon): """ Represents the event of a new message. @@ -173,7 +256,9 @@ class NewMessage(_EventBuilder): another session) or incoming (i.e. someone else sent it). """ def __init__(self, message): - self._client = None + super().__init__(chat_peer=message.to_id, + msg_id=message.id, broadcast=bool(message.post)) + self.message = message self._text = None @@ -182,13 +267,6 @@ class NewMessage(_EventBuilder): self._input_sender = None self._sender = None - self.is_private = isinstance(message.to_id, types.PeerUser) - self.is_group = ( - isinstance(message.to_id, (types.PeerChat, types.PeerChannel)) - and not message.post - ) - self.is_channel = isinstance(message.to_id, types.PeerChannel) - self.is_reply = bool(message.reply_to_msg_id) self._reply_message = None @@ -208,54 +286,6 @@ class NewMessage(_EventBuilder): reply_to=self.message.id, *args, **kwargs) - def _get_input_entity(self, msg_id, entity_id, chat=None): - """ - Helper function to call GetMessages on the give msg_id and - return the input entity whose ID is the given entity ID. - """ - try: - if isinstance(chat, types.InputPeerChannel): - result = self._client( - functions.channels.GetMessagesRequest(chat, [msg_id]) - ) - else: - result = self._client( - functions.messages.GetMessagesRequest([msg_id]) - ) - except RPCError: - return - entity = { - utils.get_peer_id(x): x for x in itertools.chain( - getattr(result, 'chats', []), - getattr(result, 'users', [])) - }.get(entity_id) - if entity: - return utils.get_input_peer(entity) - - @property - def input_chat(self): - if self._input_chat is None: - try: - self._input_chat = self._client.get_input_entity( - self.message.to_id - ) - except (ValueError, TypeError): - # The library hasn't seen this chat, get the message - if not isinstance(self.message.to_id, types.PeerChannel): - # TODO For channels, getDifference? Maybe looking - # in the dialogs (which is already done) is enough. - self._input_chat = self._get_input_entity( - self.message.id, - utils.get_peer_id(self.message.to_id) - ) - return self._input_chat - - @property - def chat(self): - if self._chat is None and self.input_chat: - self._chat = self._client.get_entity(self._input_chat) - return self._chat - @property def input_sender(self): if self._input_sender is None: From 379c7755581afe3f43007523d88fe3be2cb9afe0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 11:37:17 +0100 Subject: [PATCH 156/361] Add a new ChatAction Event --- telethon/events/__init__.py | 235 ++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d16a6529..31cf1f07 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -367,3 +367,238 @@ class NewMessage(_EventBuilder): @property def out(self): return self.message.out + + +class ChatAction(_EventBuilder): + """ + Represents an action in a chat (such as user joined, left, or new pin). + + Args: + chats (:obj:`entity`, optional): + May be one or more entities (username/peer/etc.). By default, + only matching chats will be handled. + + blacklist_chats (:obj:`bool`, optional): + Whether to treat the the list of chats as a blacklist (if + it matches it will NOT be handled) or a whitelist (default). + + """ + def __init__(self, chats=None, blacklist_chats=False): + # TODO This can probably be reused in all builders + self.chats = chats + self.blacklist_chats = blacklist_chats + + def resolve(self, client): + if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): + self.chats = set(utils.get_peer_id(x) + for x in client.get_input_entity(self.chats)) + elif self.chats is not None: + self.chats = {utils.get_peer_id( + client.get_input_entity(self.chats))} + + def build(self, update): + if isinstance(update, types.UpdateChannelPinnedMessage): + # Telegram sends UpdateChannelPinnedMessage and then + # UpdateNewChannelMessage with MessageActionPinMessage. + event = ChatAction.Event(types.PeerChannel(update.channel_id), + new_pin=update.id) + + elif isinstance(update, types.UpdateChatParticipantAdd): + event = ChatAction.Event(types.PeerChat(update.chat_id), + added_by=update.inviter_id or True, + users=update.user_id) + + elif isinstance(update, types.UpdateChatParticipantDelete): + event = ChatAction.Event(types.PeerChat(update.chat_id), + kicked_by=True, + users=update.user_id) + + elif (isinstance(update, ( + types.UpdateNewMessage, types.UpdateNewChannelMessage)) + and isinstance(update.message, types.MessageService)): + msg = update.message + action = update.message.action + if isinstance(action, types.MessageActionChatJoinedByLink): + event = ChatAction.Event(msg.to_id, + added_by=True, + users=msg.from_id) + elif isinstance(action, types.MessageActionChatAddUser): + event = ChatAction.Event(msg.to_id, + added_by=msg.from_id or True, + users=action.users) + elif isinstance(action, types.MessageActionChatDeleteUser): + event = ChatAction.Event(msg.to_id, + kicked_by=msg.from_id or True, + users=action.user_id) + elif isinstance(action, types.MessageActionChatCreate): + event = ChatAction.Event(msg.to_id, + users=action.users, + created=True, + new_title=action.title) + elif isinstance(action, types.MessageActionChannelCreate): + event = ChatAction.Event(msg.to_id, + created=True, + new_title=action.title) + elif isinstance(action, types.MessageActionChatEditTitle): + event = ChatAction.Event(msg.to_id, + new_title=action.title) + elif isinstance(action, types.MessageActionChatEditPhoto): + event = ChatAction.Event(msg.to_id, + new_photo=action.photo) + elif isinstance(action, types.MessageActionChatDeletePhoto): + event = ChatAction.Event(msg.to_id, + new_photo=True) + else: + return + else: + return + + if self.chats is None: + return event + else: + inside = utils.get_peer_id(event._chat_peer) in self.chats + if inside == self.blacklist_chats: + # If this chat matches but it's a blacklist ignore. + # If it doesn't match but it's a whitelist ignore. + return + + return event + + class Event(_EventCommon): + """ + Represents the event of a new chat action. + + Members: + new_pin (:obj:`bool`): + ``True`` if the pin has changed (new pin or removed). + + new_photo (:obj:`bool`): + ``True`` if there's a new chat photo (or it was removed). + + photo (:obj:`Photo`, optional): + The new photo (or ``None`` if it was removed). + + + user_added (:obj:`bool`): + ``True`` if the user was added by some other. + + user_joined (:obj:`bool`): + ``True`` if the user joined on their own. + + user_left (:obj:`bool`): + ``True`` if the user left on their own. + + user_kicked (:obj:`bool`): + ``True`` if the user was kicked by some other. + + created (:obj:`bool`, optional): + ``True`` if this chat was just created. + + new_title (:obj:`bool`, optional): + The new title string for the chat, if applicable. + """ + def __init__(self, chat_peer, new_pin=None, new_photo=None, + added_by=None, kicked_by=None, created=None, + users=None, new_title=None): + super().__init__(chat_peer=chat_peer, msg_id=new_pin) + + self.new_pin = isinstance(new_pin, int) + self._pinned_message = new_pin + + self.new_photo = new_photo is not None + self.photo = \ + new_photo if isinstance(new_photo, types.Photo) else None + + self._added_by = None + self._kicked_by = None + self.user_added, self.user_joined, self.user_left,\ + self.user_kicked = (False, False, False, False) + + if added_by is True: + self.user_joined = True + elif added_by: + self.user_added = True + self._added_by = added_by + + if kicked_by is True: + self.user_left = True + elif kicked_by: + self.user_kicked = True + self._kicked_by = kicked_by + + self.created = bool(created) + self._user_peers = users if isinstance(users, list) else [users] + self._users = None + self.new_title = new_title + + @property + def pinned_message(self): + """ + If ``new_pin`` is ``True``, this returns the (:obj:`Message`) + object that was pinned. + """ + if self._pinned_message == 0: + return None + + if isinstance(self._pinned_message, int) and self.input_chat: + r = self._client(functions.channels.GetMessagesRequest( + self._input_chat, [self._pinned_message] + )) + try: + self._pinned_message = next( + x for x in r.messages + if isinstance(x, types.Message) + and x.id == self._pinned_message + ) + except StopIteration: + pass + + if isinstance(self._pinned_message, types.Message): + return self._pinned_message + + @property + def added_by(self): + """ + The user who added ``users``, if applicable (``None`` otherwise). + """ + if self._added_by and not isinstance(self._added_by, types.User): + self._added_by = self._client.get_entity(self._added_by) + return self._added_by + + @property + def kicked_by(self): + """ + The user who kicked ``users``, if applicable (``None`` otherwise). + """ + if self._kicked_by and not isinstance(self._kicked_by, types.User): + self._kicked_by = self._client.get_entity(self._kicked_by) + return self._kicked_by + + @property + def user(self): + """ + The single user that takes part in this action (e.g. joined). + + Might be ``None`` if the information can't be retrieved or + there is no user taking part. + """ + try: + return next(self.users) + except (StopIteration, TypeError): + return None + + @property + def users(self): + """ + A list of users that take part in this action (e.g. joined). + + Might be empty if the information can't be retrieved or there + are no users taking part. + """ + if self._users is None and self._user_peers: + try: + self._users = self._client.get_entity(self._user_peers) + except (TypeError, ValueError): + self._users = [] + + return self._users From ffe826b35ff3fff12bbe2e16b3a7c7fb4a35b960 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 12:42:04 +0100 Subject: [PATCH 157/361] Add a new UserUpdate Event --- telethon/events/__init__.py | 151 ++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 31cf1f07..22665cf1 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,4 +1,5 @@ import abc +import datetime import itertools from .. import utils @@ -602,3 +603,153 @@ class ChatAction(_EventBuilder): self._users = [] return self._users + + +class UserUpdate(_EventBuilder): + """ + Represents an user update (gone online, offline, joined Telegram). + """ + + def build(self, update): + if isinstance(update, types.UpdateUserStatus): + event = UserUpdate.Event(update.user_id, + status=update.status) + else: + return + + return event + + def resolve(self, client): + pass + + class Event(_EventCommon): + """ + Represents the event of an user status update (last seen, joined). + + Members: + online (:obj:`bool`, optional): + ``True`` if the user is currently online, ``False`` otherwise. + Might be ``None`` if this information is not present. + + last_seen (:obj:`datetime`, optional): + Exact date when the user was last seen if known. + + until (:obj:`datetime`, optional): + Until when will the user remain online. + + within_months (:obj:`bool`): + ``True`` if the user was seen within 30 days. + + within_weeks (:obj:`bool`): + ``True`` if the user was seen within 7 days. + + recently (:obj:`bool`): + ``True`` if the user was seen within a day. + + action (:obj:`SendMessageAction`, optional): + The "typing" action if any the user is performing if any. + + cancel (:obj:`bool`): + ``True`` if the action was cancelling other actions. + + typing (:obj:`bool`): + ``True`` if the action is typing a message. + + recording (:obj:`bool`): + ``True`` if the action is recording something. + + uploading (:obj:`bool`): + ``True`` if the action is uploading something. + + playing (:obj:`bool`): + ``True`` if the action is playing a game. + + audio (:obj:`bool`): + ``True`` if what's being recorded/uploaded is an audio. + + round (:obj:`bool`): + ``True`` if what's being recorded/uploaded is a round video. + + video (:obj:`bool`): + ``True`` if what's being recorded/uploaded is an video. + + document (:obj:`bool`): + ``True`` if what's being uploaded is document. + + geo (:obj:`bool`): + ``True`` if what's being uploaded is a geo. + + photo (:obj:`bool`): + ``True`` if what's being uploaded is a photo. + + contact (:obj:`bool`): + ``True`` if what's being uploaded (selected) is a contact. + """ + def __init__(self, user_id, status=None, typing=None): + super().__init__(types.PeerUser(user_id)) + + self.online = None if status is None else \ + isinstance(status, types.UserStatusOnline) + + self.last_seen = status.was_online if \ + isinstance(status, types.UserStatusOffline) else None + + self.until = status.expires if \ + isinstance(status, types.UserStatusOnline) else None + + if self.last_seen: + diff = datetime.datetime.now() - self.last_seen + if diff < datetime.timedelta(days=30): + self.within_months = True + if diff < datetime.timedelta(days=7): + self.within_weeks = True + if diff < datetime.timedelta(days=1): + self.recently = True + else: + self.within_months = self.within_weeks = self.recently = False + if isinstance(status, (types.UserStatusOnline, + types.UserStatusRecently)): + self.within_months = self.within_weeks = True + self.recently = True + elif isinstance(status, types.UserStatusLastWeek): + self.within_months = self.within_weeks = True + elif isinstance(status, types.UserStatusLastMonth): + self.within_months = True + + self.action = typing + if typing: + self.cancel = self.typing = self.recording = self.uploading = \ + self.playing = False + self.audio = self.round = self.video = self.document = \ + self.geo = self.photo = self.contact = False + + if isinstance(typing, types.SendMessageCancelAction): + self.cancel = True + elif isinstance(typing, types.SendMessageTypingAction): + self.typing = True + elif isinstance(typing, types.SendMessageGamePlayAction): + self.playing = True + elif isinstance(typing, types.SendMessageGeoLocationAction): + self.geo = True + elif isinstance(typing, types.SendMessageRecordAudioAction): + self.recording = self.audio = True + elif isinstance(typing, types.SendMessageRecordRoundAction): + self.recording = self.round = True + elif isinstance(typing, types.SendMessageRecordVideoAction): + self.recording = self.video = True + elif isinstance(typing, types.SendMessageChooseContactAction): + self.uploading = self.contact = True + elif isinstance(typing, types.SendMessageUploadAudioAction): + self.uploading = self.audio = True + elif isinstance(typing, types.SendMessageUploadDocumentAction): + self.uploading = self.document = True + elif isinstance(typing, types.SendMessageUploadPhotoAction): + self.uploading = self.photo = True + elif isinstance(typing, types.SendMessageUploadRoundAction): + self.uploading = self.round = True + elif isinstance(typing, types.SendMessageUploadVideoAction): + self.uploading = self.video = True + + @property + def user(self): + return self.chat From 8786a5225777624dd41b5761f992c934de00319a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 13:05:34 +0100 Subject: [PATCH 158/361] Add a new MessageChanged Event --- telethon/events/__init__.py | 91 +++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 22665cf1..068ef031 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -753,3 +753,94 @@ class UserUpdate(_EventBuilder): @property def user(self): return self.chat + + +class MessageChanged(_EventBuilder): + """ + Represents a message changed (edited or deleted). + """ + + def build(self, update): + if isinstance(update, (types.UpdateEditMessage, + types.UpdateEditChannelMessage)): + event = MessageChanged.Event(edit_msg=update.message) + elif isinstance(update, (types.UpdateDeleteMessages, + types.UpdateDeleteChannelMessages)): + event = MessageChanged.Event( + deleted_ids=update.messages, + peer=types.PeerChannel(update.channel_id) + ) + else: + return + + return event + + def resolve(self, client): + pass + + class Event(_EventCommon): + """ + Represents the event of an user status update (last seen, joined). + + Members: + edited (:obj:`bool`): + ``True`` if the message was edited. + + message (:obj:`Message`, optional): + The new edited message, if any. + + deleted (:obj:`bool`): + ``True`` if the message IDs were deleted. + + deleted_ids (:obj:`List[int]`): + A list containing the IDs of the messages that were deleted. + + input_sender (:obj:`InputPeer`): + This is the input version of the user who edited the message. + Similarly to ``input_chat``, this doesn't have things like + username or similar, but still useful in some cases. + + Note that this might not be available if the library can't + find the input chat. + + sender (:obj:`User`): + This property will make an API call the first time to get the + most up to date version of the sender, so use with care as + there is no caching besides local caching yet. + + ``input_sender`` needs to be available (often the case). + """ + def __init__(self, edit_msg=None, deleted_ids=None, peer=None): + super().__init__(peer if not edit_msg else edit_msg.to_id) + + self.edited = bool(edit_msg) + self.message = edit_msg + self.deleted = bool(deleted_ids) + self.deleted_ids = deleted_ids or [] + self._input_sender = None + self._sender = None + + @property + def input_sender(self): + # TODO Code duplication + if self._input_sender is None and self.message: + try: + self._input_sender = self._client.get_input_entity( + self.message.from_id + ) + except (ValueError, TypeError): + if isinstance(self.message.to_id, types.PeerChannel): + # We can rely on self.input_chat for this + self._input_sender = self._get_input_entity( + self.message.id, + self.message.from_id, + chat=self.input_chat + ) + + return self._client.get_input_entity(self.message.from_id) + + @property + def sender(self): + if self._sender is None and self.input_sender: + self._sender = self._client.get_entity(self._input_sender) + return self._sender From f5eda72329aa4ee20f348d5123841b6122ebf2eb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 13:08:09 +0100 Subject: [PATCH 159/361] Add a new Raw Event --- telethon/events/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 068ef031..697e8056 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -102,6 +102,17 @@ class _EventCommon(abc.ABC): return self._chat +class Raw(_EventBuilder): + """ + Represents a raw event. The event is the update itself. + """ + def resolve(self, client): + pass + + def build(self, update): + return update + + # Classes defined here are actually Event builders # for their inner Event classes. Inner ._client is # set later by the creator TelegramClient. From 51677543682a0be0b394fd29c90ee27d08154942 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 13:10:02 +0100 Subject: [PATCH 160/361] Fix input_sender events' property not using cached value --- telethon/events/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 697e8056..0697521f 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -314,7 +314,7 @@ class NewMessage(_EventBuilder): chat=self.input_chat ) - return self._client.get_input_entity(self.message.from_id) + return self._input_sender @property def sender(self): @@ -848,7 +848,7 @@ class MessageChanged(_EventBuilder): chat=self.input_chat ) - return self._client.get_input_entity(self.message.from_id) + return self._input_sender @property def sender(self): From 14389a0ef221c9335ed19163e476a69a422e5551 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 15:56:42 +0100 Subject: [PATCH 161/361] Better document the events module --- readthedocs/telethon.events.rst | 8 +++ readthedocs/telethon.rst | 8 +++ telethon/events/__init__.py | 109 ++++++++++++++++++-------------- 3 files changed, 76 insertions(+), 49 deletions(-) create mode 100644 readthedocs/telethon.events.rst diff --git a/readthedocs/telethon.events.rst b/readthedocs/telethon.events.rst new file mode 100644 index 00000000..7cd062db --- /dev/null +++ b/readthedocs/telethon.events.rst @@ -0,0 +1,8 @@ +telethon\.events package +======================== + + +.. automodule:: telethon.events + :members: + :undoc-members: + :show-inheritance: diff --git a/readthedocs/telethon.rst b/readthedocs/telethon.rst index e7a30c42..96becc9b 100644 --- a/readthedocs/telethon.rst +++ b/readthedocs/telethon.rst @@ -26,6 +26,14 @@ telethon\.telegram\_client module :undoc-members: :show-inheritance: +telethon\.events package +------------------------ + +.. toctree:: + + telethon.events + + telethon\.update\_state module ------------------------------ diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 0697521f..40678998 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -206,22 +206,6 @@ class NewMessage(_EventBuilder): message (:obj:`Message`): This is the original ``Message`` object. - input_chat (:obj:`InputPeer`): - This is the input chat (private, group, megagroup or channel) - to which the message was sent. This doesn't have the title or - anything, but is useful if you don't need those to avoid - further requests. - - Note that this might not be available if the library can't - find the input chat. - - chat (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional): - This property will make an API call the first time to get the - most up to date version of the chat, so use with care as - there is no caching besides local caching yet. - - ``input_chat`` needs to be available (often the case). - is_private (:obj:`bool`): True if the message was sent as a private message. @@ -231,41 +215,8 @@ class NewMessage(_EventBuilder): is_channel (:obj:`bool`): True if the message was sent on a megagroup or channel. - input_sender (:obj:`InputPeer`): - This is the input version of the user who sent the message. - Similarly to ``input_chat``, this doesn't have things like - username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat. - - sender (:obj:`User`): - This property will make an API call the first time to get the - most up to date version of the sender, so use with care as - there is no caching besides local caching yet. - - ``input_sender`` needs to be available (often the case). - - text (:obj:`str`): - The message text, markdown-formatted. - - raw_text (:obj:`str`): - The raw message text, ignoring any formatting. - is_reply (:obj:`str`): Whether the message is a reply to some other or not. - - reply_message (:obj:`Message`, optional): - This property will make an API call the first time to get the - full ``Message`` object that one was replying to, so use with - care as there is no caching besides local caching yet. - - forward (:obj:`MessageFwdHeader`, optional): - The unmodified ``MessageFwdHeader``, if present. - - out (:obj:`bool`): - Whether the message is outgoing (i.e. you sent it from - another session) or incoming (i.e. someone else sent it). """ def __init__(self, message): super().__init__(chat_peer=message.to_id, @@ -300,6 +251,14 @@ class NewMessage(_EventBuilder): @property def input_sender(self): + """ + This (:obj:`InputPeer`) is the input version of the user who + sent the message. Similarly to ``input_chat``, this doesn't have + things like username or similar, but still useful in some cases. + + Note that this might not be available if the library can't + find the input chat. + """ if self._input_sender is None: try: self._input_sender = self._client.get_input_entity( @@ -318,12 +277,22 @@ class NewMessage(_EventBuilder): @property def sender(self): + """ + This (:obj:`User`) will make an API call the first time to get + the most up to date version of the sender, so use with care as + there is no caching besides local caching yet. + + ``input_sender`` needs to be available (often the case). + """ if self._sender is None and self.input_sender: self._sender = self._client.get_entity(self._input_sender) return self._sender @property def text(self): + """ + The message text, markdown-formatted. + """ if self._text is None: if not self.message.entities: return self.message.message @@ -333,10 +302,18 @@ class NewMessage(_EventBuilder): @property def raw_text(self): + """ + The raw message text, ignoring any formatting. + """ return self.message.message @property def reply_message(self): + """ + This (:obj:`Message`, optional) will make an API call the first + time to get the full ``Message`` object that one was replying to, + so use with care as there is no caching besides local caching yet. + """ if not self.message.reply_to_msg_id: return None @@ -356,14 +333,24 @@ class NewMessage(_EventBuilder): @property def forward(self): + """ + The unmodified (:obj:`MessageFwdHeader`, optional). + """ return self.message.fwd_from @property def media(self): + """ + The unmodified (:obj:`MessageMedia`, optional). + """ return self.message.media @property def photo(self): + """ + If the message media is a photo, + this returns the (:obj:`Photo`) object. + """ if isinstance(self.message.media, types.MessageMediaPhoto): photo = self.message.media.photo if isinstance(photo, types.Photo): @@ -371,6 +358,10 @@ class NewMessage(_EventBuilder): @property def document(self): + """ + If the message media is a document, + this returns the (:obj:`Document`) object. + """ if isinstance(self.message.media, types.MessageMediaDocument): doc = self.message.media.document if isinstance(doc, types.Document): @@ -378,6 +369,10 @@ class NewMessage(_EventBuilder): @property def out(self): + """ + Whether the message is outgoing (i.e. you sent it from + another session) or incoming (i.e. someone else sent it). + """ return self.message.out @@ -763,6 +758,7 @@ class UserUpdate(_EventBuilder): @property def user(self): + """Alias around the chat (conversation).""" return self.chat @@ -833,6 +829,14 @@ class MessageChanged(_EventBuilder): @property def input_sender(self): + """ + This (:obj:`InputPeer`) is the input version of the user who + sent the message. Similarly to ``input_chat``, this doesn't have + things like username or similar, but still useful in some cases. + + Note that this might not be available if the library can't + find the input chat. + """ # TODO Code duplication if self._input_sender is None and self.message: try: @@ -852,6 +856,13 @@ class MessageChanged(_EventBuilder): @property def sender(self): + """ + This (:obj:`User`) will make an API call the first time to get + the most up to date version of the sender, so use with care as + there is no caching besides local caching yet. + + ``input_sender`` needs to be available (often the case). + """ if self._sender is None and self.input_sender: self._sender = self._client.get_entity(self._input_sender) return self._sender From 10ebc442c9342262c0bfdb65155a19fdce9be9f4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 16:41:23 +0100 Subject: [PATCH 162/361] Add a friendlier introduction to events --- .../extra/advanced-usage/update-modes.rst | 144 +++++++++++++ .../extra/basic/working-with-updates.rst | 204 ++++++++---------- readthedocs/index.rst | 1 + readthedocs/telethon.events.rst | 4 - 4 files changed, 240 insertions(+), 113 deletions(-) create mode 100644 readthedocs/extra/advanced-usage/update-modes.rst diff --git a/readthedocs/extra/advanced-usage/update-modes.rst b/readthedocs/extra/advanced-usage/update-modes.rst new file mode 100644 index 00000000..83495ef7 --- /dev/null +++ b/readthedocs/extra/advanced-usage/update-modes.rst @@ -0,0 +1,144 @@ +.. _update-modes: + +============ +Update Modes +============ + + +The library can run in four distinguishable modes: + +- With no extra threads at all. +- With an extra thread that receives everything as soon as possible (default). +- With several worker threads that run your update handlers. +- A mix of the above. + +Since this section is about updates, we'll describe the simplest way to +work with them. + + +Using multiple workers +********************** + +When you create your client, simply pass a number to the +``update_workers`` parameter: + + ``client = TelegramClient('session', api_id, api_hash, update_workers=2)`` + +You can set any amount of workers you want. The more you put, the more +update handlers that can be called "at the same time". One or two should +suffice most of the time, since setting more will not make things run +faster most of the times (actually, it could slow things down). + +The next thing you want to do is to add a method that will be called when +an `Update`__ arrives: + + .. code-block:: python + + def callback(update): + print('I received', update) + + client.add_update_handler(callback) + # do more work here, or simply sleep! + +That's it! This is the old way to listen for raw updates, with no further +processing. If this feels annoying for you, remember that you can always +use :ref:`working-with-updates` but maybe use this for some other cases. + +Now let's do something more interesting. Every time an user talks to use, +let's reply to them with the same text reversed: + + .. code-block:: python + + from telethon.tl.types import UpdateShortMessage, PeerUser + + def replier(update): + if isinstance(update, UpdateShortMessage) and not update.out: + client.send_message(PeerUser(update.user_id), update.message[::-1]) + + + client.add_update_handler(replier) + input('Press enter to stop this!') + client.disconnect() + +We only ask you one thing: don't keep this running for too long, or your +contacts will go mad. + + +Spawning no worker at all +************************* + +All the workers do is loop forever and poll updates from a queue that is +filled from the ``ReadThread``, responsible for reading every item off +the network. If you only need a worker and the ``MainThread`` would be +doing no other job, this is the preferred way. You can easily do the same +as the workers like so: + + .. code-block:: python + + while True: + try: + update = client.updates.poll() + if not update: + continue + + print('I received', update) + except KeyboardInterrupt: + break + + client.disconnect() + +Note that ``poll`` accepts a ``timeout=`` parameter, and it will return +``None`` if other thread got the update before you could or if the timeout +expired, so it's important to check ``if not update``. + +This can coexist with the rest of ``N`` workers, or you can set it to ``0`` +additional workers: + + ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` + +You **must** set it to ``0`` (or other number), as it defaults to ``None`` +and there is a different. ``None`` workers means updates won't be processed +*at all*, so you must set it to some value (``0`` or greater) if you want +``client.updates.poll()`` to work. + + +Using the main thread instead the ``ReadThread`` +************************************************ + +If you have no work to do on the ``MainThread`` and you were planning to have +a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary +``ReadThread`` at all like so: + + .. code-block:: python + + client = TelegramClient( + ... + spawn_read_thread=False + ) + +And then ``.idle()`` from the ``MainThread``: + + ``client.idle()`` + +You can stop it with :kbd:`Control+C`, and you can configure the signals +to be used in a similar fashion to `Python Telegram Bot`__. + +As a complete example: + + .. code-block:: python + + def callback(update): + print('I received', update) + + client = TelegramClient('session', api_id, api_hash, + update_workers=1, spawn_read_thread=False) + + client.connect() + client.add_update_handler(callback) + client.idle() # ends with Ctrl+C + + +This is the preferred way to use if you're simply going to listen for updates. + +__ https://lonamiwebs.github.io/Telethon/types/update.html +__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 72155d86..a6c0a529 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -5,144 +5,130 @@ Working with Updates ==================== -.. note:: - - There are plans to make working with updates more friendly. Stay tuned! +The library comes with the :mod:`events` module. *Events* are an abstraction +over what Telegram calls `updates`__, and are meant to ease simple and common +usage when dealing with them, since there are many updates. Let's dive in! .. contents:: -The library can run in four distinguishable modes: - -- With no extra threads at all. -- With an extra thread that receives everything as soon as possible (default). -- With several worker threads that run your update handlers. -- A mix of the above. - -Since this section is about updates, we'll describe the simplest way to -work with them. - - -Using multiple workers -********************** - -When you create your client, simply pass a number to the -``update_workers`` parameter: - - ``client = TelegramClient('session', api_id, api_hash, update_workers=4)`` - -4 workers should suffice for most cases (this is also the default on -`Python Telegram Bot`__). You can set this value to more, or even less -if you need. - -The next thing you want to do is to add a method that will be called when -an `Update`__ arrives: +Getting Started +*************** .. code-block:: python - def callback(update): - print('I received', update) + from telethon import TelegramClient, events - client.add_update_handler(callback) - # do more work here, or simply sleep! + client = TelegramClient(..., update_workers=1, spawn_read_thread=False) + client.start() -That's it! Now let's do something more interesting. -Every time an user talks to use, let's reply to them with the same -text reversed: + @client.on(events.NewMessage) + def my_event_handler(event): + if 'hello' in event.raw_text: + event.reply('hi!') + + client.idle() + + +Not much, but there might be some things unclear. What does this code do? .. code-block:: python - from telethon.tl.types import UpdateShortMessage, PeerUser + from telethon import TelegramClient, events - def replier(update): - if isinstance(update, UpdateShortMessage) and not update.out: - client.send_message(PeerUser(update.user_id), update.message[::-1]) + client = TelegramClient(..., update_workers=1, spawn_read_thread=False) + client.start() - client.add_update_handler(replier) - input('Press enter to stop this!') - client.disconnect() - -We only ask you one thing: don't keep this running for too long, or your -contacts will go mad. - - -Spawning no worker at all -************************* - -All the workers do is loop forever and poll updates from a queue that is -filled from the ``ReadThread``, responsible for reading every item off -the network. If you only need a worker and the ``MainThread`` would be -doing no other job, this is the preferred way. You can easily do the same -as the workers like so: +This is normal initialization (of course, pass session name, API ID and hash). +Nothing we don't know already. .. code-block:: python - while True: - try: - update = client.updates.poll() - if not update: - continue - - print('I received', update) - except KeyboardInterrupt: - break - - client.disconnect() - -Note that ``poll`` accepts a ``timeout=`` parameter, and it will return -``None`` if other thread got the update before you could or if the timeout -expired, so it's important to check ``if not update``. - -This can coexist with the rest of ``N`` workers, or you can set it to ``0`` -additional workers: - - ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` - -You **must** set it to ``0`` (or other number), as it defaults to ``None`` -and there is a different. ``None`` workers means updates won't be processed -*at all*, so you must set it to some value (``0`` or greater) if you want -``client.updates.poll()`` to work. + @client.on(events.NewMessage) -Using the main thread instead the ``ReadThread`` -************************************************ - -If you have no work to do on the ``MainThread`` and you were planning to have -a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary -``ReadThread`` at all like so: +This Python decorator will attach itself to the ``my_event_handler`` +definition, and basically means that *on* a ``NewMessage`` *event*, +the callback function you're about to define will be called: .. code-block:: python - client = TelegramClient( - ... - spawn_read_thread=False - ) + def my_event_handler(event): + if 'hello' in event.raw_text: + event.reply('hi!') -And then ``.idle()`` from the ``MainThread``: - ``client.idle()`` - -You can stop it with :kbd:`Control+C`, and you can configure the signals -to be used in a similar fashion to `Python Telegram Bot`__. - -As a complete example: +If a ``NewMessage`` event occurs, and ``'hello'`` is in the text of the +message, we ``reply`` to the event with a ``'hi!'`` message. .. code-block:: python - def callback(update): - print('I received', update) - - client = TelegramClient('session', api_id, api_hash, - update_workers=1, spawn_read_thread=False) - - client.connect() - client.add_update_handler(callback) - client.idle() # ends with Ctrl+C - client.disconnect() + client.idle() + + +Finally, this tells the client that we're done with our code, and want +to listen for all these events to occur. Of course, you might want to +do other things instead idling. For this refer to :ref:`update-modes`. + + +More on events +************** + +The ``NewMessage`` event has much more than what was shown. You can access +the ``.sender`` of the message through that member, or even see if the message +had ``.media``, a ``.photo`` or a ``.document`` (which you could download with +for example ``client.download_media(event.photo)``. + +If you don't want to ``.reply`` as a reply, you can use the ``.respond()`` +method instead. Of course, there are more events such as ``ChatAction`` or +``UserUpdate``, and they're all used in the same way. Simply add the +``@client.on(events.XYZ)`` decorator on the top of your handler and you're +done! The event that will be passed always is of type ``XYZ.Event`` (for +instance, ``NewMessage.Event``), except for the ``Raw`` event which just +passes the ``Update`` object. + +You can put the same event on many handlers, and even different events on +the same handler. You can also have a handler work on only specific chats, +for example: + + + .. code-block:: python + + import ast + import random + + + @client.on(events.NewMessage(chats='TelethonOffTopic', incoming=True)) + def normal_handler(event): + if 'roll' in event.raw_text: + event.reply(str(random.randint(1, 6))) + + + @client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True)) + def admin_handler(event): + if event.raw_text.startswith('eval'): + expression = event.raw_text.replace('eval', '').strip() + event.reply(str(ast.literal_eval(expression))) + + +You can pass one or more chats to the ``chats`` parameter (as a list or tuple), +and only events from there will be processed. You can also specify whether you +want to handle incoming or outgoing messages (those you receive or those you +send). In this example, people can say ``'roll'`` and you will reply with a +random number, while if you say ``'eval 4+4'``, you will reply with the +solution. Try it! + + +Events module +************* + +.. automodule:: telethon.events + :members: + :undoc-members: + :show-inheritance: + -__ https://python-telegram-bot.org/ __ https://lonamiwebs.github.io/Telethon/types/update.html -__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 74c3b8e6..c1d2b6ec 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -49,6 +49,7 @@ heavy job for you, so you can focus on developing an application. extra/advanced-usage/accessing-the-full-api extra/advanced-usage/sessions + extra/advanced-usage/update-modes .. _Examples: diff --git a/readthedocs/telethon.events.rst b/readthedocs/telethon.events.rst index 7cd062db..071a39bf 100644 --- a/readthedocs/telethon.events.rst +++ b/readthedocs/telethon.events.rst @@ -2,7 +2,3 @@ telethon\.events package ======================== -.. automodule:: telethon.events - :members: - :undoc-members: - :show-inheritance: From 6261affaa1bf1223705e967bd01f1d76b680208e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 17:16:28 +0100 Subject: [PATCH 163/361] Update to v0.17.1 --- readthedocs/extra/advanced-usage/.DS_Store | Bin 6148 -> 0 bytes readthedocs/extra/changelog.rst | 12 ++++++++++++ telethon/version.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) delete mode 100644 readthedocs/extra/advanced-usage/.DS_Store diff --git a/readthedocs/extra/advanced-usage/.DS_Store b/readthedocs/extra/advanced-usage/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Fri, 9 Feb 2018 19:39:20 +0100 Subject: [PATCH 164/361] Fix sign up method not accepting integer codes --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c4f5d722..1e90824f 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -416,7 +416,7 @@ class TelegramClient(TelegramBareClient): result = self(SignUpRequest( phone_number=self._phone, phone_code_hash=self._phone_code_hash.get(self._phone, ''), - phone_code=code, + phone_code=str(code), first_name=first_name, last_name=last_name )) From 7d8d86c5f1b5ac0aa6a9a4d9485786d3d53fc0e7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 10 Feb 2018 10:26:58 +0100 Subject: [PATCH 165/361] Support inline mentions (bot API style or username/phone) --- telethon/telegram_client.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 1e90824f..f4efaa55 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -3,6 +3,7 @@ import io import itertools import logging import os +import re import sys import time from collections import OrderedDict, UserList @@ -68,7 +69,8 @@ from .tl.types import ( PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, - InputDocument, InputMediaDocument, Document + InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, + InputMessageEntityMentionName ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -603,6 +605,20 @@ class TelegramClient(TelegramBareClient): message, msg_entities = html.parse(message) else: raise ValueError('Unknown parsing mode: {}'.format(parse_mode)) + + for i, e in enumerate(msg_entities): + if isinstance(e, MessageEntityTextUrl): + m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url) + if m: + try: + msg_entities[i] = InputMessageEntityMentionName( + e.offset, e.length, self.get_input_entity( + int(m.group(1)) if m.group(1) else e.url + ) + ) + except (ValueError, TypeError): + # Make no replacement + pass else: msg_entities = [] From eca1e8ec877812c5f7b699c3b7daaccf49ceeda0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 10 Feb 2018 10:45:43 +0100 Subject: [PATCH 166/361] Default to markdown parse mode on send_message This is consistent with official clients and also provide the expected result when replying to NewMessage events. --- telethon/telegram_client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f4efaa55..e5689fad 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -570,7 +570,7 @@ class TelegramClient(TelegramBareClient): if update.message.id == msg_id: return update.message - def send_message(self, entity, message, reply_to=None, parse_mode=None, + def send_message(self, entity, message, reply_to=None, parse_mode='md', link_preview=True): """ Sends the given message to the specified entity (user/chat/channel). @@ -587,8 +587,10 @@ class TelegramClient(TelegramBareClient): it should be the ID of the message that it should reply to. parse_mode (:obj:`str`, optional): - Can be 'md' or 'markdown' for markdown-like parsing, in a similar - fashion how official clients work. + Can be 'md' or 'markdown' for markdown-like parsing (default), + or 'htm' or 'html' for HTML-like parsing. If ``None`` or any + other false-y value is provided, the message will be sent with + no formatting. link_preview (:obj:`bool`, optional): Should the link preview be shown? From 0633e204c219a86665ba515124959af9edccada7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 11 Feb 2018 10:30:45 +0100 Subject: [PATCH 167/361] Fix whitelisting multiple chats on events not working --- telethon/events/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 40678998..a22a757e 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -149,8 +149,8 @@ class NewMessage(_EventBuilder): def resolve(self, client): if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(x) - for x in client.get_input_entity(self.chats)) + self.chats = set(utils.get_peer_id(client.get_input_entity(x)) + for x in self.chats) elif self.chats is not None: self.chats = {utils.get_peer_id( client.get_input_entity(self.chats))} @@ -397,8 +397,8 @@ class ChatAction(_EventBuilder): def resolve(self, client): if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(x) - for x in client.get_input_entity(self.chats)) + self.chats = set(utils.get_peer_id(client.get_input_entity(x)) + for x in self.chats) elif self.chats is not None: self.chats = {utils.get_peer_id( client.get_input_entity(self.chats))} From 9abeefac7f276336b9235bd3b160427e227ccea6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 12 Feb 2018 10:33:51 +0100 Subject: [PATCH 168/361] Send video files as video by default instead as document (#601) --- telethon/telegram_client.py | 9 ++++++--- telethon/utils.py | 11 ++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e5689fad..c0c1bb1e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -70,7 +70,7 @@ from .tl.types import ( ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, - InputMessageEntityMentionName + InputMessageEntityMentionName, DocumentAttributeVideo ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -922,8 +922,8 @@ class TelegramClient(TelegramBareClient): force_document (:obj:`bool`, optional): If left to ``False`` and the file is a path that ends with - ``.png``, ``.jpg`` and such, the file will be sent as a photo. - Otherwise always as a document. + the extension of an image file or a video file, it will be + sent as such. Otherwise always as a document. progress_callback (:obj:`callable`, optional): A callback function accepting two parameters: @@ -1015,6 +1015,9 @@ class TelegramClient(TelegramBareClient): # TODO If the input file is an audio, find out: # Performer and song title and add DocumentAttributeAudio } + if not force_document and utils.is_video(file): + attr_dict[DocumentAttributeVideo] = \ + DocumentAttributeVideo(0, 0, 0) else: attr_dict = { DocumentAttributeFilename: diff --git a/telethon/utils.py b/telethon/utils.py index 3e310d3d..9460986c 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -3,10 +3,10 @@ Utilities for working with the Telegram API itself (such as handy methods to convert between an entity like an User, Chat, etc. into its Input version) """ import math +import mimetypes import re from mimetypes import add_type, guess_extension -from .tl.types.contacts import ResolvedPeer from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty, @@ -24,6 +24,7 @@ from .tl.types import ( InputMediaUploadedPhoto, DocumentAttributeFilename, photos, TopPeer, InputNotifyPeer ) +from .tl.types.contacts import ResolvedPeer USERNAME_RE = re.compile( r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' @@ -322,8 +323,12 @@ def get_input_media(media, user_caption=None, is_photo=False): def is_image(file): """Returns True if the file extension looks like an image file""" - return (isinstance(file, str) and - bool(re.search(r'\.(png|jpe?g|gif)$', file, re.IGNORECASE))) + return (mimetypes.guess_type(file)[0] or '').startswith('image/') + + +def is_video(file): + """Returns True if the file extension looks like a video file""" + return (mimetypes.guess_type(file)[0] or '').startswith('video/') def parse_phone(phone): From c83638ed0e168985f8edefd2cb2b1c266a288174 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 12 Feb 2018 13:40:00 +0100 Subject: [PATCH 169/361] Add further logging calls to better spot lost requests --- telethon/network/mtproto_sender.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 877611df..43b5e803 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -86,6 +86,11 @@ class MtProtoSender: messages = [TLMessage(self.session, r) for r in requests] self._pending_receive.update({m.msg_id: m for m in messages}) + __log__.debug('Sending requests with IDs: %s', ', '.join( + '{}: {}'.format(m.request.__class__.__name__, m.msg_id) + for m in messages + )) + # Pack everything in the same container if we need to send AckRequests if self._need_confirmation: messages.append( @@ -465,6 +470,7 @@ class MtProtoSender: request_id = reader.read_long() inner_code = reader.read_int(signed=False) + __log__.debug('Received response for request with ID %d', request_id) request = self._pop_request(request_id) if inner_code == 0x2144ca19: # RPC Error @@ -502,8 +508,18 @@ class MtProtoSender: return True # If it's really a result for RPC from previous connection - # session, it will be skipped by the handle_container() - __log__.warning('Lost request will be skipped') + # session, it will be skipped by the handle_container(). + # For some reason this also seems to happen when downloading + # photos, where the server responds with FileJpeg(). + try: + obj = reader.tgread_object() + except Exception as e: + obj = '(failed to read: %s)' % e + + __log__.warning( + 'Lost request (ID %d) with code %s will be skipped, contents: %s', + request_id, hex(inner_code), obj + ) return False def _handle_gzip_packed(self, msg_id, sequence, reader, state): From 08b9d7c4ef034370cd6733643cda167e0b1c6d48 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 13 Feb 2018 10:24:35 +0100 Subject: [PATCH 170/361] Add more logic to better retrieve input_sender on events --- telethon/events/__init__.py | 44 +++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index a22a757e..a02385c1 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -136,6 +136,11 @@ class NewMessage(_EventBuilder): blacklist_chats (:obj:`bool`, optional): Whether to treat the the list of chats as a blacklist (if it matches it will NOT be handled) or a whitelist (default). + + Notes: + The ``message.from_id`` might not only be an integer or ``None``, + but also ``InputPeerSelf()`` for short private messages (the API + would not return such thing, this is a custom modification). """ def __init__(self, incoming=None, outgoing=None, chats=None, blacklist_chats=False): @@ -169,6 +174,7 @@ class NewMessage(_EventBuilder): silent=update.silent, id=update.id, to_id=types.PeerUser(update.user_id), + from_id=types.InputPeerSelf() if update.out else update.user_id, message=update.message, date=update.date, fwd_from=update.fwd_from, @@ -257,21 +263,23 @@ class NewMessage(_EventBuilder): things like username or similar, but still useful in some cases. Note that this might not be available if the library can't - find the input chat. + find the input chat, or if the message a broadcast on a channel. """ if self._input_sender is None: + if self.is_channel and not self.is_group: + return None + try: self._input_sender = self._client.get_input_entity( self.message.from_id ) except (ValueError, TypeError): - if isinstance(self.message.to_id, types.PeerChannel): - # We can rely on self.input_chat for this - self._input_sender = self._get_input_entity( - self.message.id, - self.message.from_id, - chat=self.input_chat - ) + # We can rely on self.input_chat for this + self._input_sender = self._get_input_entity( + self.message.id, + self.message.from_id, + chat=self.input_chat + ) return self._input_sender @@ -835,22 +843,24 @@ class MessageChanged(_EventBuilder): things like username or similar, but still useful in some cases. Note that this might not be available if the library can't - find the input chat. + find the input chat, or if the message a broadcast on a channel. """ # TODO Code duplication - if self._input_sender is None and self.message: + if self._input_sender is None: + if self.is_channel and not self.is_group: + return None + try: self._input_sender = self._client.get_input_entity( self.message.from_id ) except (ValueError, TypeError): - if isinstance(self.message.to_id, types.PeerChannel): - # We can rely on self.input_chat for this - self._input_sender = self._get_input_entity( - self.message.id, - self.message.from_id, - chat=self.input_chat - ) + # We can rely on self.input_chat for this + self._input_sender = self._get_input_entity( + self.message.id, + self.message.from_id, + chat=self.input_chat + ) return self._input_sender From 55bcc29ae0834e202a0040dd72c1116cb4a9f5c7 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Wed, 14 Feb 2018 17:09:22 +1000 Subject: [PATCH 171/361] Errors: Fix passing 'self' to the constructors of the superclasses This is necessary only if the superclass name is specified explicitly instead of super() call. --- telethon/errors/common.py | 8 +++----- telethon/errors/rpc_base_errors.py | 8 ++++---- telethon_generator/error_generator.py | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/telethon/errors/common.py b/telethon/errors/common.py index 46b0b52e..0c03aee6 100644 --- a/telethon/errors/common.py +++ b/telethon/errors/common.py @@ -4,7 +4,7 @@ class ReadCancelledError(Exception): """Occurs when a read operation was cancelled.""" def __init__(self): - super().__init__(self, 'The read operation was cancelled.') + super().__init__('The read operation was cancelled.') class TypeNotFoundError(Exception): @@ -14,7 +14,7 @@ class TypeNotFoundError(Exception): """ def __init__(self, invalid_constructor_id): super().__init__( - self, 'Could not find a matching Constructor ID for the TLObject ' + 'Could not find a matching Constructor ID for the TLObject ' 'that was supposed to be read with ID {}. Most likely, a TLObject ' 'was trying to be read when it should not be read.' .format(hex(invalid_constructor_id))) @@ -29,7 +29,6 @@ class InvalidChecksumError(Exception): """ def __init__(self, checksum, valid_checksum): super().__init__( - self, 'Invalid checksum ({} when {} was expected). ' 'This packet should be skipped.' .format(checksum, valid_checksum)) @@ -44,7 +43,6 @@ class BrokenAuthKeyError(Exception): """ def __init__(self): super().__init__( - self, 'The authorization key is broken, and it must be reset.' ) @@ -56,7 +54,7 @@ class SecurityError(Exception): def __init__(self, *args): if not args: args = ['A security check failed.'] - super().__init__(self, *args) + super().__init__(*args) class CdnFileTamperedError(SecurityError): diff --git a/telethon/errors/rpc_base_errors.py b/telethon/errors/rpc_base_errors.py index 9e6eed1a..467b256c 100644 --- a/telethon/errors/rpc_base_errors.py +++ b/telethon/errors/rpc_base_errors.py @@ -40,7 +40,7 @@ class ForbiddenError(RPCError): message = 'FORBIDDEN' def __init__(self, message): - super().__init__(self, message) + super().__init__(message) self.message = message @@ -52,7 +52,7 @@ class NotFoundError(RPCError): message = 'NOT_FOUND' def __init__(self, message): - super().__init__(self, message) + super().__init__(message) self.message = message @@ -77,7 +77,7 @@ class ServerError(RPCError): message = 'INTERNAL' def __init__(self, message): - super().__init__(self, message) + super().__init__(message) self.message = message @@ -121,7 +121,7 @@ class BadMessageError(Exception): } def __init__(self, code): - super().__init__(self, self.ErrorMessages.get( + super().__init__(self.ErrorMessages.get( code, 'Unknown error code (this should not happen): {}.'.format(code))) diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py index a56d4b91..73fb5c5a 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -68,7 +68,7 @@ def write_error(f, code, name, desc, capture_name): f.write( "self.{} = int(kwargs.get('capture', 0))\n ".format(capture_name) ) - f.write('super(Exception, self).__init__(self, {}'.format(repr(desc))) + f.write('super(Exception, self).__init__({}'.format(repr(desc))) if capture_name: f.write('.format(self.{})'.format(capture_name)) f.write(')\n') From e9dd93f09cc2232c388479fb1fa366adef51af51 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Feb 2018 14:06:45 +0100 Subject: [PATCH 172/361] Further clarify the documentation (flood wait, lists and inputs) --- docs/generate.py | 13 ++++++++++++- docs/res/css/docs.css | 4 ++++ readthedocs/extra/basic/creating-a-client.rst | 5 +++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/generate.py b/docs/generate.py index ae2bd43c..75ab3091 100755 --- a/docs/generate.py +++ b/docs/generate.py @@ -207,6 +207,13 @@ def get_description(arg): desc.append('This argument can be omitted.') otherwise = True + if arg.type in {'InputPeer', 'InputUser', 'InputChannel'}: + desc.append( + 'Anything entity-like will work if the library can find its ' + 'Input version (e.g., usernames, Peer, ' + 'User or Channel objects, etc.).' + ) + if arg.is_vector: if arg.is_generic: desc.append('A list of other Requests must be supplied.') @@ -221,7 +228,11 @@ def get_description(arg): desc.insert(1, 'Otherwise,') desc[-1] = desc[-1][:1].lower() + desc[-1][1:] - return ' '.join(desc) + return ' '.join(desc).replace( + 'list', + 'list' + ) def copy_replace(src, dst, replacements): diff --git a/docs/res/css/docs.css b/docs/res/css/docs.css index 05c61c9f..cd67af70 100644 --- a/docs/res/css/docs.css +++ b/docs/res/css/docs.css @@ -108,6 +108,10 @@ span.sh4 { color: #06c; } +span.tooltip { + border-bottom: 1px dashed #444; +} + #searchBox { width: 100%; border: none; diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index bf565bb0..e68f170b 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -95,6 +95,11 @@ is just a matter of taste, and how much control you need. Remember that you can get yourself at any time with ``client.get_me()``. +.. warning:: + Please note that if you fail to login around 5 times (or change the first + parameter of the ``TelegramClient``, which is the session name) you will + receive a ``FloodWaitError`` of around 22 hours, so be careful not to mess + this up! This shouldn't happen if you're doing things as explained, though. .. note:: If you want to use a **proxy**, you have to `install PySocks`__ From 80f918956a147c53e7878323f0f9b03d27475397 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Feb 2018 10:02:12 +0100 Subject: [PATCH 173/361] Revert "official apps must be obfuscated" (29471f3) It didn't really make any sense and @danog keeps changing it. --- 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 5984bb2e..db07f321 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -103,8 +103,6 @@ class TelegramBareClient: self.session = session self.api_id = int(api_id) self.api_hash = api_hash - if self.api_id < 20: # official apps must use obfuscated - connection_mode = ConnectionMode.TCP_OBFUSCATED # This is the main sender, which will be used from the thread # that calls .connect(). Every other thread will spawn a new From 62c057a0582247bc9b6787a318e4481181bf66af Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Feb 2018 11:19:34 +0100 Subject: [PATCH 174/361] Add edit_message convenience method and refactor to accomodate it --- telethon/telegram_client.py | 147 ++++++++++++++++++++++++++---------- 1 file changed, 109 insertions(+), 38 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c0c1bb1e..e54690e9 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -46,8 +46,8 @@ from .tl.functions.contacts import ( from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, - CheckChatInviteRequest, ReadMentionsRequest, - SendMultiMediaRequest, UploadMediaRequest + CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, + UploadMediaRequest, EditMessageRequest ) from .tl.functions import channels @@ -70,7 +70,8 @@ from .tl.types import ( ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, - InputMessageEntityMentionName, DocumentAttributeVideo + InputMessageEntityMentionName, DocumentAttributeVideo, + UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -565,11 +566,60 @@ class TelegramClient(TelegramBareClient): msg_id = update.id break - for update in result.updates: + if isinstance(result, UpdateShort): + updates = [result.update] + elif isinstance(result, Updates): + updates = result.updates + else: + return + + for update in updates: if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)): if update.message.id == msg_id: return update.message + elif (isinstance(update, UpdateEditMessage) and + not isinstance(request.peer, InputPeerChannel)): + if request.id == update.message.id: + return update.message + + elif (isinstance(update, UpdateEditChannelMessage) and + utils.get_peer_id(request.peer) == + utils.get_peer_id(update.message.to_id)): + if request.id == update.message.id: + return update.message + + def _parse_message_text(self, message, parse_mode): + """ + Returns a (parsed message, entities) tuple depending on parse_mode. + """ + if not parse_mode: + return message, [] + + parse_mode = parse_mode.lower() + if parse_mode in {'md', 'markdown'}: + message, msg_entities = markdown.parse(message) + elif parse_mode.startswith('htm'): + message, msg_entities = html.parse(message) + else: + raise ValueError('Unknown parsing mode: {}'.format(parse_mode)) + + for i, e in enumerate(msg_entities): + if isinstance(e, MessageEntityTextUrl): + m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url) + if m: + try: + msg_entities[i] = InputMessageEntityMentionName( + e.offset, e.length, self.get_input_entity( + int(m.group(1)) if m.group(1) else e.url + ) + ) + except (ValueError, TypeError): + # Make no replacement + pass + + return message, msg_entities + def send_message(self, entity, message, reply_to=None, parse_mode='md', link_preview=True): """ @@ -599,37 +649,14 @@ class TelegramClient(TelegramBareClient): the sent message """ entity = self.get_input_entity(entity) - if parse_mode: - parse_mode = parse_mode.lower() - if parse_mode in {'md', 'markdown'}: - message, msg_entities = markdown.parse(message) - elif parse_mode.startswith('htm'): - message, msg_entities = html.parse(message) - else: - raise ValueError('Unknown parsing mode: {}'.format(parse_mode)) - - for i, e in enumerate(msg_entities): - if isinstance(e, MessageEntityTextUrl): - m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url) - if m: - try: - msg_entities[i] = InputMessageEntityMentionName( - e.offset, e.length, self.get_input_entity( - int(m.group(1)) if m.group(1) else e.url - ) - ) - except (ValueError, TypeError): - # Make no replacement - pass - else: - msg_entities = [] + message, msg_entities = self._parse_message_text(message, parse_mode) request = SendMessageRequest( peer=entity, message=message, entities=msg_entities, no_webpage=not link_preview, - reply_to_msg_id=self._get_reply_to(reply_to) + reply_to_msg_id=self._get_message_id(reply_to) ) result = self(request) if isinstance(result, UpdateShortSentMessage): @@ -645,6 +672,50 @@ class TelegramClient(TelegramBareClient): return self._get_response_message(request, result) + def edit_message(self, entity, message_id, message=None, parse_mode='md', + link_preview=True): + """ + Edits the given message ID (to change its contents or disable preview). + + Args: + entity (:obj:`entity`): + From which chat to edit the message. + + message_id (:obj:`str`): + The ID of the message (or ``Message`` itself) to be edited. + + message (:obj:`str`, optional): + The new text of the message. + + parse_mode (:obj:`str`, optional): + Can be 'md' or 'markdown' for markdown-like parsing (default), + or 'htm' or 'html' for HTML-like parsing. If ``None`` or any + other false-y value is provided, the message will be sent with + no formatting. + + link_preview (:obj:`bool`, optional): + Should the link preview be shown? + + Raises: + ``MessageAuthorRequiredError`` if you're not the author of the + message but try editing it anyway. + + ``MessageNotModifiedError`` if the contents of the message were + not modified at all. + + Returns: + the edited message + """ + message, msg_entities = self._parse_message_text(message, parse_mode) + request = EditMessageRequest( + peer=self.get_input_entity(entity), + id=self._get_message_id(message_id), + message=message, + no_webpage=not link_preview + ) + result = self(request) + return self._get_response_message(request, result) + def delete_messages(self, entity, message_ids, revoke=True): """ Deletes a message from a chat, optionally "for everyone". @@ -869,22 +940,22 @@ class TelegramClient(TelegramBareClient): return False @staticmethod - def _get_reply_to(reply_to): + def _get_message_id(message): """Sanitizes the 'reply_to' parameter a user may send""" - if reply_to is None: + if message is None: return None - if isinstance(reply_to, int): - return reply_to + if isinstance(message, int): + return message try: - if reply_to.SUBCLASS_OF_ID == 0x790009e3: + if message.SUBCLASS_OF_ID == 0x790009e3: # hex(crc32(b'Message')) = 0x790009e3 - return reply_to.id + return message.id except AttributeError: pass - raise TypeError('Invalid reply_to type: {}'.format(type(reply_to))) + raise TypeError('Invalid message type: {}'.format(type(message))) # endregion @@ -973,7 +1044,7 @@ class TelegramClient(TelegramBareClient): ] entity = self.get_input_entity(entity) - reply_to = self._get_reply_to(reply_to) + reply_to = self._get_message_id(reply_to) if not isinstance(file, (str, bytes, io.IOBase)): # The user may pass a Message containing media (or the media, @@ -1086,7 +1157,7 @@ class TelegramClient(TelegramBareClient): # cache only makes a difference for documents where the user may # want the attributes used on them to change. Caption's ignored. entity = self.get_input_entity(entity) - reply_to = self._get_reply_to(reply_to) + reply_to = self._get_message_id(reply_to) # Need to upload the media first, but only if they're not cached yet media = [] From 196275e9c8e3aa1e48ee258be5557dfc02e91090 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Feb 2018 11:35:12 +0100 Subject: [PATCH 175/361] Add edit and delete shorthand methods to events.NewMessage --- telethon/events/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index a02385c1..3a888200 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -255,6 +255,32 @@ class NewMessage(_EventBuilder): reply_to=self.message.id, *args, **kwargs) + def edit(self, *args, **kwargs): + """ + Edits the message iff it's outgoing. This is a shorthand for + ``client.edit_message(event.chat, event.message, ...)``. + + Returns ``None`` if the message was incoming, + or the edited message otherwise. + """ + if not self.message.out: + return None + + return self._client.edit_message(self.input_chat, + self.message, + *args, **kwargs) + + def delete(self, *args, **kwargs): + """ + Deletes the message. You're responsible for checking whether you + have the permission to do so, or to except the error otherwise. + This is a shorthand for + ``client.delete_messages(event.chat, event.message, ...)``. + """ + return self._client.delete_messages(self.input_chat, + [self.message], + *args, **kwargs) + @property def input_sender(self): """ From 178643d3a1951688ee16a32ee33d89ed53d4f14a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Feb 2018 11:41:32 +0100 Subject: [PATCH 176/361] Periodically send getState even without disconnect (341fb38) After some more tests, even if the server doesn't drop the connection, it might also just stop sending updates at all. --- telethon/telegram_bare_client.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index db07f321..31a3f7d9 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -158,6 +158,14 @@ class TelegramBareClient: self._last_ping = datetime.now() self._ping_delay = timedelta(minutes=1) + # Also have another delay for GetStateRequest. + # + # If the connection is kept alive for long without invoking any + # high level request the server simply stops sending updates. + # TODO maybe we can have ._last_request instead if any req works? + self._last_state = datetime.now() + self._state_delay = timedelta(hours=1) + # Some errors are known but there's nothing we can do from the # background thread. If any of these happens, call .disconnect(), # and raise them next time .invoke() is tried to be called. @@ -579,6 +587,7 @@ class TelegramBareClient: otherwise it should be called manually after enabling updates. """ self.updates.process(self(GetStateRequest())) + self._last_state = datetime.now() def add_update_handler(self, handler): """Adds an update handler (a function which takes a TLObject, @@ -650,6 +659,10 @@ class TelegramBareClient: )) self._last_ping = datetime.now() + if datetime.now() > self._last_state + self._state_delay: + self._sender.send(GetStateRequest()) + self._last_state = datetime.now() + __log__.debug('Receiving items from the network...') self._sender.receive(update_state=self.updates) except TimeoutError: From 75d99fbb53c1bb7d46f9b9909c056b6e00eab4d8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Feb 2018 11:52:46 +0100 Subject: [PATCH 177/361] Fix HTML entity parsing failing when needing surrogates --- telethon/extensions/html.py | 23 +++++++++++++++++++---- telethon/extensions/markdown.py | 3 +++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/telethon/extensions/html.py b/telethon/extensions/html.py index 8cd170cb..bcbd13cc 100644 --- a/telethon/extensions/html.py +++ b/telethon/extensions/html.py @@ -1,9 +1,10 @@ """ Simple HTML -> Telegram entity parser. """ +import struct +from collections import deque from html import escape, unescape from html.parser import HTMLParser -from collections import deque from ..tl.types import ( MessageEntityBold, MessageEntityItalic, MessageEntityCode, @@ -12,6 +13,18 @@ from ..tl.types import ( ) +# Helpers from markdown.py +def _add_surrogate(text): + return ''.join( + ''.join(chr(y) for y in struct.unpack(' Date: Thu, 15 Feb 2018 12:11:26 +0100 Subject: [PATCH 178/361] Update to v0.17.2 --- readthedocs/extra/changelog.rst | 35 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 57b11bec..34609615 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,41 @@ it can take advantage of new goodies! .. contents:: List of All Versions +New small convenience functions (v0.17.2) +========================================= + +*Published at 2018/02/15* + +Primarily bug fixing and a few welcomed additions. + +Additions +~~~~~~~~~ + +- New convenience ``.edit_message()`` method on the ``TelegramClient``. +- New ``.edit()`` and ``.delete()`` shorthands on the ``NewMessage`` event. +- Default to markdown parsing when sending and editing messages. +- Support for inline mentions when sending and editing messages. They work + like inline urls (e.g. ``[text](@username)``) and also support the Bot-API + style (see `here `__). + +Bug fixes +~~~~~~~~~ + +- Periodically send ``GetStateRequest`` automatically to keep the server + sending updates even if you're not invoking any request yourself. +- HTML parsing was failing due to not handling surrogates properly. +- ``.sign_up`` was not accepting ``int`` codes. +- Whitelisting more than one chat on ``events`` wasn't working. +- Video files are sent as a video by default unless ``force_document``. + +Internal changes +~~~~~~~~~~~~~~~~ + +- More ``logging`` calls to help spot some bugs in the future. +- Some more logic to retrieve input entities on events. +- Clarified a few parts of the documentation. + + Updates as Events (v0.17.1) =========================== diff --git a/telethon/version.py b/telethon/version.py index 6cf35eba..4ead720b 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.17.1' +__version__ = '0.17.2' From c11aefa95be711540b167abc3442101ed628c6f2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Feb 2018 13:42:12 +0100 Subject: [PATCH 179/361] Fix message entities being ignored by edit_message --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e54690e9..7de7b59a 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -711,7 +711,8 @@ class TelegramClient(TelegramBareClient): peer=self.get_input_entity(entity), id=self._get_message_id(message_id), message=message, - no_webpage=not link_preview + no_webpage=not link_preview, + entities=msg_entities ) result = self(request) return self._get_response_message(request, result) From 030f29220328693e08bc9bc7148b07ddc22f7aef Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Feb 2018 18:24:44 +0100 Subject: [PATCH 180/361] Dump libssl bindings in favour of the new optional cryptg module --- telethon/crypto/aes.py | 124 ++++++++++++++++++++------------------ telethon/crypto/libssl.py | 107 -------------------------------- 2 files changed, 64 insertions(+), 167 deletions(-) delete mode 100644 telethon/crypto/libssl.py diff --git a/telethon/crypto/aes.py b/telethon/crypto/aes.py index 191cde15..8f13b5f0 100644 --- a/telethon/crypto/aes.py +++ b/telethon/crypto/aes.py @@ -3,86 +3,90 @@ AES IGE implementation in Python. This module may use libssl if available. """ import os import pyaes -from . import libssl + +try: + import cryptg +except ImportError: + cryptg = None -if libssl.AES is not None: - # Use libssl if available, since it will be faster - AES = libssl.AES -else: - # Fallback to a pure Python implementation - class AES: +class AES: + """ + Class that servers as an interface to encrypt and decrypt + text through the AES IGE mode. + """ + @staticmethod + def decrypt_ige(cipher_text, key, iv): """ - Class that servers as an interface to encrypt and decrypt - text through the AES IGE mode. + Decrypts the given text in 16-bytes blocks by using the + given key and 32-bytes initialization vector. """ - @staticmethod - def decrypt_ige(cipher_text, key, iv): - """ - Decrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ - iv1 = iv[:len(iv) // 2] - iv2 = iv[len(iv) // 2:] + if cryptg: + return cryptg.decrypt_ige(cipher_text, key, iv) - aes = pyaes.AES(key) + iv1 = iv[:len(iv) // 2] + iv2 = iv[len(iv) // 2:] - plain_text = [] - blocks_count = len(cipher_text) // 16 + aes = pyaes.AES(key) - cipher_text_block = [0] * 16 - for block_index in range(blocks_count): - for i in range(16): - cipher_text_block[i] = \ - cipher_text[block_index * 16 + i] ^ iv2[i] + plain_text = [] + blocks_count = len(cipher_text) // 16 - plain_text_block = aes.decrypt(cipher_text_block) + cipher_text_block = [0] * 16 + for block_index in range(blocks_count): + for i in range(16): + cipher_text_block[i] = \ + cipher_text[block_index * 16 + i] ^ iv2[i] - for i in range(16): - plain_text_block[i] ^= iv1[i] + plain_text_block = aes.decrypt(cipher_text_block) - iv1 = cipher_text[block_index * 16:block_index * 16 + 16] - iv2 = plain_text_block + for i in range(16): + plain_text_block[i] ^= iv1[i] - plain_text.extend(plain_text_block) + iv1 = cipher_text[block_index * 16:block_index * 16 + 16] + iv2 = plain_text_block - return bytes(plain_text) + plain_text.extend(plain_text_block) - @staticmethod - def encrypt_ige(plain_text, key, iv): - """ - Encrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ + return bytes(plain_text) - # Add random padding iff it's not evenly divisible by 16 already - if len(plain_text) % 16 != 0: - padding_count = 16 - len(plain_text) % 16 - plain_text += os.urandom(padding_count) + @staticmethod + def encrypt_ige(plain_text, key, iv): + """ + Encrypts the given text in 16-bytes blocks by using the + given key and 32-bytes initialization vector. + """ + # Add random padding iff it's not evenly divisible by 16 already + if len(plain_text) % 16 != 0: + padding_count = 16 - len(plain_text) % 16 + plain_text += os.urandom(padding_count) - iv1 = iv[:len(iv) // 2] - iv2 = iv[len(iv) // 2:] + if cryptg: + return cryptg.encrypt_ige(plain_text, key, iv) - aes = pyaes.AES(key) + iv1 = iv[:len(iv) // 2] + iv2 = iv[len(iv) // 2:] - cipher_text = [] - blocks_count = len(plain_text) // 16 + aes = pyaes.AES(key) - for block_index in range(blocks_count): - plain_text_block = list( - plain_text[block_index * 16:block_index * 16 + 16] - ) - for i in range(16): - plain_text_block[i] ^= iv1[i] + cipher_text = [] + blocks_count = len(plain_text) // 16 - cipher_text_block = aes.encrypt(plain_text_block) + for block_index in range(blocks_count): + plain_text_block = list( + plain_text[block_index * 16:block_index * 16 + 16] + ) + for i in range(16): + plain_text_block[i] ^= iv1[i] - for i in range(16): - cipher_text_block[i] ^= iv2[i] + cipher_text_block = aes.encrypt(plain_text_block) - iv1 = cipher_text_block - iv2 = plain_text[block_index * 16:block_index * 16 + 16] + for i in range(16): + cipher_text_block[i] ^= iv2[i] - cipher_text.extend(cipher_text_block) + iv1 = cipher_text_block + iv2 = plain_text[block_index * 16:block_index * 16 + 16] - return bytes(cipher_text) + cipher_text.extend(cipher_text_block) + + return bytes(cipher_text) diff --git a/telethon/crypto/libssl.py b/telethon/crypto/libssl.py deleted file mode 100644 index b4735112..00000000 --- a/telethon/crypto/libssl.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -This module holds an AES IGE class, if libssl is available on the system. -""" -import os -import ctypes -from ctypes.util import find_library - -lib = find_library('ssl') -if not lib: - AES = None -else: - """ - # define AES_ENCRYPT 1 - # define AES_DECRYPT 0 - # define AES_MAXNR 14 - struct aes_key_st { - # ifdef AES_LONG - unsigned long rd_key[4 * (AES_MAXNR + 1)]; - # else - unsigned int rd_key[4 * (AES_MAXNR + 1)]; - # endif - int rounds; - }; - typedef struct aes_key_st AES_KEY; - - int AES_set_encrypt_key(const unsigned char *userKey, const int bits, - AES_KEY *key); - int AES_set_decrypt_key(const unsigned char *userKey, const int bits, - AES_KEY *key); - void AES_ige_encrypt(const unsigned char *in, unsigned char *out, - size_t length, const AES_KEY *key, - unsigned char *ivec, const int enc); - """ - _libssl = ctypes.cdll.LoadLibrary(lib) - - AES_MAXNR = 14 - AES_ENCRYPT = ctypes.c_int(1) - AES_DECRYPT = ctypes.c_int(0) - - class AES_KEY(ctypes.Structure): - """Helper class representing an AES key""" - _fields_ = [ - ('rd_key', ctypes.c_uint32 * (4*(AES_MAXNR + 1))), - ('rounds', ctypes.c_uint), - ] - - class AES: - """ - Class that servers as an interface to encrypt and decrypt - text through the AES IGE mode, using the system's libssl. - """ - @staticmethod - def decrypt_ige(cipher_text, key, iv): - """ - Decrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ - aeskey = AES_KEY() - ckey = (ctypes.c_ubyte * len(key))(*key) - cklen = ctypes.c_int(len(key)*8) - cin = (ctypes.c_ubyte * len(cipher_text))(*cipher_text) - ctlen = ctypes.c_size_t(len(cipher_text)) - cout = (ctypes.c_ubyte * len(cipher_text))() - civ = (ctypes.c_ubyte * len(iv))(*iv) - - _libssl.AES_set_decrypt_key(ckey, cklen, ctypes.byref(aeskey)) - _libssl.AES_ige_encrypt( - ctypes.byref(cin), - ctypes.byref(cout), - ctlen, - ctypes.byref(aeskey), - ctypes.byref(civ), - AES_DECRYPT - ) - - return bytes(cout) - - @staticmethod - def encrypt_ige(plain_text, key, iv): - """ - Encrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ - # Add random padding iff it's not evenly divisible by 16 already - if len(plain_text) % 16 != 0: - padding_count = 16 - len(plain_text) % 16 - plain_text += os.urandom(padding_count) - - aeskey = AES_KEY() - ckey = (ctypes.c_ubyte * len(key))(*key) - cklen = ctypes.c_int(len(key)*8) - cin = (ctypes.c_ubyte * len(plain_text))(*plain_text) - ctlen = ctypes.c_size_t(len(plain_text)) - cout = (ctypes.c_ubyte * len(plain_text))() - civ = (ctypes.c_ubyte * len(iv))(*iv) - - _libssl.AES_set_encrypt_key(ckey, cklen, ctypes.byref(aeskey)) - _libssl.AES_ige_encrypt( - ctypes.byref(cin), - ctypes.byref(cout), - ctlen, - ctypes.byref(aeskey), - ctypes.byref(civ), - AES_ENCRYPT - ) - - return bytes(cout) From 83d9d1d78e536b77834b27b05c5e6789268b2e7a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Feb 2018 20:30:19 +0100 Subject: [PATCH 181/361] Fix markdown parser not inverting delimiters dict --- telethon/extensions/markdown.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 92589a17..e1fd04b7 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -169,6 +169,7 @@ def unparse(text, entities, delimiters=None, url_fmt=None): entities = tuple(sorted(entities, key=lambda e: e.offset, reverse=True)) text = _add_surrogate(text) + delimiters = {v: k for k, v in delimiters.items()} for entity in entities: s = entity.offset e = entity.offset + entity.length From 3c6f34fe6a801f084ebb66dc210d501f3b93aa8e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Feb 2018 21:02:47 +0100 Subject: [PATCH 182/361] Update examples --- telethon_examples/print_updates.py | 34 +++---- telethon_examples/replier.py | 137 +++++++++-------------------- 2 files changed, 55 insertions(+), 116 deletions(-) diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py index ab7ba1d4..4c676a81 100755 --- a/telethon_examples/print_updates.py +++ b/telethon_examples/print_updates.py @@ -1,46 +1,36 @@ #!/usr/bin/env python3 # A simple script to print all updates received -from getpass import getpass from os import environ + # environ is used to get API information from environment variables # You could also use a config file, pass them as arguments, # or even hardcode them (not recommended) from telethon import TelegramClient -from telethon.errors import SessionPasswordNeededError + def main(): session_name = environ.get('TG_SESSION', 'session') - user_phone = environ['TG_PHONE'] client = TelegramClient(session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], proxy=None, - update_workers=4) + update_workers=4, + spawn_read_thread=False) - print('INFO: Connecting to Telegram Servers...', end='', flush=True) - client.connect() - print('Done!') - - if not client.is_user_authorized(): - print('INFO: Unauthorized user') - client.send_code_request(user_phone) - code_ok = False - while not code_ok: - code = input('Enter the auth code: ') - try: - code_ok = client.sign_in(user_phone, code) - except SessionPasswordNeededError: - password = getpass('Two step verification enabled. Please enter your password: ') - code_ok = client.sign_in(password=password) - print('INFO: Client initialized succesfully!') + if 'TG_PHONE' in environ: + client.start(phone=environ['TG_PHONE']) + else: + client.start() client.add_update_handler(update_handler) - input('Press Enter to stop this!\n') + print('(Press Ctrl+C to stop this)') + client.idle() + def update_handler(update): print(update) - print('Press Enter to stop this!') + if __name__ == '__main__': main() diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py index 66026363..ed4cc2fa 100755 --- a/telethon_examples/replier.py +++ b/telethon_examples/replier.py @@ -9,17 +9,12 @@ file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION. This script assumes that you have certain files on the working directory, such as "xfiles.m4a" or "anytime.png" for some of the automated replies. """ -from getpass import getpass +import re from collections import defaultdict from datetime import datetime, timedelta from os import environ -import re - -from telethon import TelegramClient -from telethon.errors import SessionPasswordNeededError -from telethon.tl.types import UpdateNewChannelMessage, UpdateShortMessage, MessageService -from telethon.tl.functions.messages import EditMessageRequest +from telethon import TelegramClient, events, utils """Uncomment this for debugging import logging @@ -35,103 +30,57 @@ REACTS = {'emacs': 'Needs more vim', recent_reacts = defaultdict(list) -def update_handler(update): - global recent_reacts - try: - msg = update.message - except AttributeError: - # print(update, 'did not have update.message') - return - if isinstance(msg, MessageService): - print(msg, 'was service msg') - return +if __name__ == '__main__': + # TG_API_ID and TG_API_HASH *must* exist or this won't run! + session_name = environ.get('TG_SESSION', 'session') + client = TelegramClient( + session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], + spawn_read_thread=False, proxy=None, update_workers=4 + ) - # React to messages in supergroups and PMs - if isinstance(update, UpdateNewChannelMessage): - words = re.split('\W+', msg.message) + @client.on(events.NewMessage) + def my_handler(event: events.NewMessage.Event): + global recent_reacts + + # This utils function gets the unique identifier from peers (to_id) + to_id = utils.get_peer_id(event.message.to_id) + + # Through event.raw_text we access the text of messages without format + words = re.split('\W+', event.raw_text) + + # Try to match some reaction for trigger, response in REACTS.items(): - if len(recent_reacts[msg.to_id.channel_id]) > 3: + if len(recent_reacts[to_id]) > 3: # Silently ignore triggers if we've recently sent 3 reactions break if trigger in words: # Remove recent replies older than 10 minutes - recent_reacts[msg.to_id.channel_id] = [ - a for a in recent_reacts[msg.to_id.channel_id] if + recent_reacts[to_id] = [ + a for a in recent_reacts[to_id] if datetime.now() - a < timedelta(minutes=10) ] - # Send a reaction - client.send_message(msg.to_id, response, reply_to=msg.id) + # Send a reaction as a reply (otherwise, event.respond()) + event.reply(response) # Add this reaction to the list of recent actions - recent_reacts[msg.to_id.channel_id].append(datetime.now()) + recent_reacts[to_id].append(datetime.now()) - if isinstance(update, UpdateShortMessage): - words = re.split('\W+', msg) - for trigger, response in REACTS.items(): - if len(recent_reacts[update.user_id]) > 3: - # Silently ignore triggers if we've recently sent 3 reactions - break + # Automatically send relevant media when we say certain things + # When invoking requests, get_input_entity needs to be called manually + if event.out: + if event.raw_text.lower() == 'x files theme': + client.send_voice_note(event.message.to_id, 'xfiles.m4a', + reply_to=event.message.id) + if event.raw_text.lower() == 'anytime': + client.send_file(event.message.to_id, 'anytime.png', + reply_to=event.message.id) + if '.shrug' in event.text: + event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯')) - if trigger in words: - # Send a reaction - client.send_message(update.user_id, response, reply_to=update.id) - # Add this reaction to the list of recent reactions - recent_reacts[update.user_id].append(datetime.now()) + if 'TG_PHONE' in environ: + client.start(phone=environ['TG_PHONE']) + else: + client.start() - # Automatically send relevant media when we say certain things - # When invoking requests, get_input_entity needs to be called manually - if isinstance(update, UpdateNewChannelMessage) and msg.out: - if msg.message.lower() == 'x files theme': - client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id) - if msg.message.lower() == 'anytime': - client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id) - if '.shrug' in msg.message: - client(EditMessageRequest( - client.get_input_entity(msg.to_id), msg.id, - message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯') - )) - - if isinstance(update, UpdateShortMessage) and update.out: - if msg.lower() == 'x files theme': - client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id) - if msg.lower() == 'anytime': - client.send_file(update.user_id, 'anytime.png', reply_to=update.id) - if '.shrug' in msg: - client(EditMessageRequest( - client.get_input_entity(update.user_id), update.id, - message=msg.replace('.shrug', r'¯\_(ツ)_/¯') - )) - - -if __name__ == '__main__': - session_name = environ.get('TG_SESSION', 'session') - user_phone = environ['TG_PHONE'] - client = TelegramClient( - session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], - proxy=None, update_workers=4 - ) - try: - print('INFO: Connecting to Telegram Servers...', end='', flush=True) - client.connect() - print('Done!') - - if not client.is_user_authorized(): - print('INFO: Unauthorized user') - client.send_code_request(user_phone) - code_ok = False - while not code_ok: - code = input('Enter the auth code: ') - try: - code_ok = client.sign_in(user_phone, code) - except SessionPasswordNeededError: - password = getpass('Two step verification enabled. ' - 'Please enter your password: ') - code_ok = client.sign_in(password=password) - print('INFO: Client initialized successfully!') - - client.add_update_handler(update_handler) - input('Press Enter to stop this!\n') - except KeyboardInterrupt: - pass - finally: - client.disconnect() + print('(Press Ctrl+C to stop this)') + client.idle() From 8718cf0e7e0d47484445ac4ce380ce0d933fdee9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 10:40:01 +0100 Subject: [PATCH 183/361] Reuse turning chats into a set of IDs and handle self case --- telethon/events/__init__.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 3a888200..b422cd26 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -8,6 +8,24 @@ from ..extensions import markdown from ..tl import types, functions +def _into_id_set(client, chats): + """Helper util to turn the input chat or chats into a set of IDs.""" + if chats is None: + return None + + if not hasattr(chats, '__iter__') or isinstance(chats, str): + chats = (chats,) + + result = set() + for chat in chats: + chat = client.get_input_entity(chat) + if isinstance(chat, types.InputPeerSelf): + chat = getattr(_into_id_set, 'me', None) or client.get_me() + _into_id_set.me = chat + result.add(utils.get_peer_id(chat)) + return result + + class _EventBuilder(abc.ABC): @abc.abstractmethod def build(self, update): @@ -153,12 +171,7 @@ class NewMessage(_EventBuilder): self.blacklist_chats = blacklist_chats def resolve(self, client): - if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(client.get_input_entity(x)) - for x in self.chats) - elif self.chats is not None: - self.chats = {utils.get_peer_id( - client.get_input_entity(self.chats))} + self.chats = _into_id_set(client, self.chats) def build(self, update): if isinstance(update, @@ -430,12 +443,7 @@ class ChatAction(_EventBuilder): self.blacklist_chats = blacklist_chats def resolve(self, client): - if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(client.get_input_entity(x)) - for x in self.chats) - elif self.chats is not None: - self.chats = {utils.get_peer_id( - client.get_input_entity(self.chats))} + self.chats = _into_id_set(client, self.chats) def build(self, update): if isinstance(update, types.UpdateChannelPinnedMessage): From 6d993af338384e7bb87ffdd4332a8545a5956211 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 11:29:16 +0100 Subject: [PATCH 184/361] Move events chats and blacklist_chats into the base, reuse code --- telethon/events/__init__.py | 99 +++++++++++++------------------------ 1 file changed, 35 insertions(+), 64 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index b422cd26..06c08db9 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -27,13 +27,42 @@ def _into_id_set(client, chats): class _EventBuilder(abc.ABC): + """ + The common event builder, with builtin support to filter per chat. + + Args: + chats (:obj:`entity`, optional): + May be one or more entities (username/peer/etc.). By default, + only matching chats will be handled. + + blacklist_chats (:obj:`bool`, optional): + Whether to treat the the list of chats as a blacklist (if + it matches it will NOT be handled) or a whitelist (default). + """ + def __init__(self, chats=None, blacklist_chats=False): + self.chats = chats + self.blacklist_chats = blacklist_chats + @abc.abstractmethod def build(self, update): """Builds an event for the given update if possible, or returns None""" - @abc.abstractmethod def resolve(self, client): """Helper method to allow event builders to be resolved before usage""" + self.chats = _into_id_set(client, self.chats) + + def _filter_event(self, event): + """ + If the ID of ``event._chat_peer`` isn't in the chats set (or it is + but the set is a blacklist) returns ``None``, otherwise the event. + """ + if self.chats is not None: + inside = utils.get_peer_id(event._chat_peer) in self.chats + if inside == self.blacklist_chats: + # If this chat matches but it's a blacklist ignore. + # If it doesn't match but it's a whitelist ignore. + return None + return event class _EventCommon(abc.ABC): @@ -124,9 +153,6 @@ class Raw(_EventBuilder): """ Represents a raw event. The event is the update itself. """ - def resolve(self, client): - pass - def build(self, update): return update @@ -147,14 +173,6 @@ class NewMessage(_EventBuilder): If set to ``True``, only **outgoing** messages will be handled. Mutually exclusive with ``incoming`` (can only set one of either). - chats (:obj:`entity`, optional): - May be one or more entities (username/peer/etc.). By default, - only matching chats will be handled. - - blacklist_chats (:obj:`bool`, optional): - Whether to treat the the list of chats as a blacklist (if - it matches it will NOT be handled) or a whitelist (default). - Notes: The ``message.from_id`` might not only be an integer or ``None``, but also ``InputPeerSelf()`` for short private messages (the API @@ -165,13 +183,9 @@ class NewMessage(_EventBuilder): if incoming and outgoing: raise ValueError('Can only set either incoming or outgoing') + super().__init__(chats=chats, blacklist_chats=blacklist_chats) self.incoming = incoming self.outgoing = outgoing - self.chats = chats - self.blacklist_chats = blacklist_chats - - def resolve(self, client): - self.chats = _into_id_set(client, self.chats) def build(self, update): if isinstance(update, @@ -207,15 +221,7 @@ class NewMessage(_EventBuilder): if self.outgoing and not event.message.out: return - if self.chats is not None: - inside = utils.get_peer_id(event.message.to_id) in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return - - # Tests passed so return the event - return event + return self._filter_event(event) class Event(_EventCommon): """ @@ -426,25 +432,7 @@ class NewMessage(_EventBuilder): class ChatAction(_EventBuilder): """ Represents an action in a chat (such as user joined, left, or new pin). - - Args: - chats (:obj:`entity`, optional): - May be one or more entities (username/peer/etc.). By default, - only matching chats will be handled. - - blacklist_chats (:obj:`bool`, optional): - Whether to treat the the list of chats as a blacklist (if - it matches it will NOT be handled) or a whitelist (default). - """ - def __init__(self, chats=None, blacklist_chats=False): - # TODO This can probably be reused in all builders - self.chats = chats - self.blacklist_chats = blacklist_chats - - def resolve(self, client): - self.chats = _into_id_set(client, self.chats) - def build(self, update): if isinstance(update, types.UpdateChannelPinnedMessage): # Telegram sends UpdateChannelPinnedMessage and then @@ -502,16 +490,7 @@ class ChatAction(_EventBuilder): else: return - if self.chats is None: - return event - else: - inside = utils.get_peer_id(event._chat_peer) in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return - - return event + return self._filter_event(event) class Event(_EventCommon): """ @@ -657,7 +636,6 @@ class UserUpdate(_EventBuilder): """ Represents an user update (gone online, offline, joined Telegram). """ - def build(self, update): if isinstance(update, types.UpdateUserStatus): event = UserUpdate.Event(update.user_id, @@ -665,10 +643,7 @@ class UserUpdate(_EventBuilder): else: return - return event - - def resolve(self, client): - pass + return self._filter_event(event) class Event(_EventCommon): """ @@ -808,7 +783,6 @@ class MessageChanged(_EventBuilder): """ Represents a message changed (edited or deleted). """ - def build(self, update): if isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): @@ -822,10 +796,7 @@ class MessageChanged(_EventBuilder): else: return - return event - - def resolve(self, client): - pass + return self._filter_event(event) class Event(_EventCommon): """ From d581589313514ece5ceb1a5ab00576ec34f6dbc9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 11:40:38 +0100 Subject: [PATCH 185/361] Add missing UpdateShortChatMessage case on events.NewMessage --- telethon/events/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 06c08db9..91683594 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -209,6 +209,22 @@ class NewMessage(_EventBuilder): reply_to_msg_id=update.reply_to_msg_id, entities=update.entities )) + elif isinstance(update, types.UpdateShortChatMessage): + event = NewMessage.Event(types.Message( + out=update.out, + mentioned=update.mentioned, + media_unread=update.media_unread, + silent=update.silent, + id=update.id, + from_id=update.from_id, + to_id=types.PeerChat(update.chat_id), + message=update.message, + date=update.date, + fwd_from=update.fwd_from, + via_bot_id=update.via_bot_id, + reply_to_msg_id=update.reply_to_msg_id, + entities=update.entities + )) else: return From 7c647b57e396f7bca492f4ff64476428cc9dc7f2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 12:14:23 +0100 Subject: [PATCH 186/361] Mention cryptg as an optional dependency --- optional-requirements.txt | 1 + readthedocs/extra/basic/installation.rst | 7 +++++++ requirements.txt | 2 ++ setup.py | 5 ++++- 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 optional-requirements.txt create mode 100644 requirements.txt diff --git a/optional-requirements.txt b/optional-requirements.txt new file mode 100644 index 00000000..5631ec03 --- /dev/null +++ b/optional-requirements.txt @@ -0,0 +1 @@ +cryptg diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index e74cdae6..0f812127 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -33,6 +33,13 @@ If you don't have root access, simply pass the ``--user`` flag to the pip command. If you want to install a specific branch, append ``@branch`` to the end of the first install command. +By default the library will use a pure Python implementation for encryption, +which can be really slow when uploading or downloading files. If you don't +mind using a C extension, install `cryptg `__ +via ``pip`` or as an extra: + + ``pip3 install telethon[cryptg]`` + Manual Installation ******************* diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..2b650ec4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyaes +rsa diff --git a/setup.py b/setup.py index 2682e099..85e77a74 100755 --- a/setup.py +++ b/setup.py @@ -149,7 +149,10 @@ def main(): 'telethon_generator', 'telethon_tests', 'run_tests.py', 'try_telethon.py' ]), - install_requires=['pyaes', 'rsa'] + install_requires=['pyaes', 'rsa'], + extras_require={ + 'cryptg': ['cryptg'] + } ) From 2bfe86cda1e2718fb458cb432e9cb3f43d09768b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 12:32:30 +0100 Subject: [PATCH 187/361] Fix bot_token could not be specified alone on .start() --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 7de7b59a..711a438f 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -267,7 +267,7 @@ class TelegramClient(TelegramBareClient): if not phone and not bot_token: raise ValueError('No phone number or bot token provided.') - if phone and bot_token: + if phone and bot_token and not callable(phone): raise ValueError('Both a phone and a bot token provided, ' 'must only provide one of either') From 33fd6895d3909f28610b5ee66a961e731a603c3a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Feb 2018 13:00:58 +0100 Subject: [PATCH 188/361] Use hachoir to determine audio and video metadata if possible Closes #611 --- optional-requirements.txt | 1 + telethon/telegram_client.py | 46 ++++++++++++++++++++++++++++++++----- telethon/utils.py | 5 ++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/optional-requirements.txt b/optional-requirements.txt index 5631ec03..c973d402 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1 +1,2 @@ cryptg +hachoir3 diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 711a438f..9888b9e5 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -23,6 +23,13 @@ try: except ImportError: socks = None +try: + import hachoir + import hachoir.metadata + import hachoir.parser +except ImportError: + hachoir = None + from . import TelegramBareClient from . import helpers, utils from .errors import ( @@ -1021,6 +1028,10 @@ class TelegramClient(TelegramBareClient): 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. + Notes: + If the ``hachoir3`` package (``hachoir`` module) is installed, + it will be used to determine metadata from audio and video files. + Returns: The message (or messages) containing the sent file. """ @@ -1084,12 +1095,32 @@ class TelegramClient(TelegramBareClient): 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 } + if utils.is_audio(file) and hachoir: + m = hachoir.metadata.extractMetadata( + hachoir.parser.createParser(file) + ) + attr_dict[DocumentAttributeAudio] = DocumentAttributeAudio( + title=m.get('title') if m.has('title') else None, + performer=m.get('author') if m.has('author') else None, + duration=int(m.get('duration').seconds + if m.has('duration') else 0) + ) + if not force_document and utils.is_video(file): - attr_dict[DocumentAttributeVideo] = \ - DocumentAttributeVideo(0, 0, 0) + if hachoir: + m = hachoir.metadata.extractMetadata( + hachoir.parser.createParser(file) + ) + doc = DocumentAttributeVideo( + w=m.get('width') if m.has('width') else 0, + h=m.get('height') if m.has('height') else 0, + duration=int(m.get('duration').seconds + if m.has('duration') else 0) + ) + else: + doc = DocumentAttributeVideo(0, 0, 0) + attr_dict[DocumentAttributeVideo] = doc else: attr_dict = { DocumentAttributeFilename: @@ -1097,8 +1128,11 @@ class TelegramClient(TelegramBareClient): } if 'is_voice_note' in kwargs: - attr_dict[DocumentAttributeAudio] = \ - DocumentAttributeAudio(0, voice=True) + if DocumentAttributeAudio in attr_dict: + attr_dict[DocumentAttributeAudio].voice = True + else: + 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 diff --git a/telethon/utils.py b/telethon/utils.py index 9460986c..2f27b79e 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -326,6 +326,11 @@ def is_image(file): return (mimetypes.guess_type(file)[0] or '').startswith('image/') +def is_audio(file): + """Returns True if the file extension looks like an audio file""" + return (mimetypes.guess_type(file)[0] or '').startswith('audio/') + + def is_video(file): """Returns True if the file extension looks like a video file""" return (mimetypes.guess_type(file)[0] or '').startswith('video/') From 1179c9e21bab3269f373d2c79b26801dc708cdac Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Feb 2018 12:17:02 +0100 Subject: [PATCH 189/361] Fix start not asking for password if needed --- telethon/telegram_client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9888b9e5..e81171d4 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,3 +1,4 @@ +import getpass import hashlib import io import itertools @@ -216,8 +217,8 @@ class TelegramClient(TelegramBareClient): def start(self, phone=lambda: input('Please enter your phone: '), - password=None, bot_token=None, - force_sms=False, code_callback=None, + password=lambda: getpass.getpass('Please enter your password: '), + bot_token=None, force_sms=False, code_callback=None, first_name='New User', last_name=''): """ Convenience method to interactively connect and sign in if required, @@ -331,6 +332,9 @@ class TelegramClient(TelegramBareClient): "Two-step verification is enabled for this account. " "Please provide the 'password' argument to 'start()'." ) + # TODO If callable given make it retry on invalid + if callable(password): + password = password() me = self.sign_in(phone=phone, password=password) # We won't reach here if any step failed (exit by exception) From b93e1b5f5033e7e850917d0cfc88f0ba23e7902d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Feb 2018 13:29:05 +0100 Subject: [PATCH 190/361] Add add_event_handler and deprecate add_update_handler --- telethon/telegram_bare_client.py | 19 ---------- telethon/telegram_client.py | 59 +++++++++++++++++++++++++------- telethon/update_state.py | 10 +++--- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 31a3f7d9..23fd4ee4 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -1,7 +1,6 @@ import logging import os import threading -import warnings from datetime import timedelta, datetime from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Lock @@ -589,24 +588,6 @@ class TelegramBareClient: self.updates.process(self(GetStateRequest())) self._last_state = datetime.now() - 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 self.updates.workers is None: - warnings.warn( - "You have not setup any workers, so you won't receive updates." - " Pass update_workers=4 when creating the TelegramClient," - " or set client.self.updates.workers = 4" - ) - - self.updates.handlers.append(handler) - - def remove_update_handler(self, handler): - self.updates.handlers.remove(handler) - - def list_update_handlers(self): - return self.updates.handlers[:] - # endregion # region Constant read diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e81171d4..118f1f53 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -7,6 +7,7 @@ import os import re import sys import time +import warnings from collections import OrderedDict, UserList from datetime import datetime, timedelta from io import BytesIO @@ -32,7 +33,7 @@ except ImportError: hachoir = None from . import TelegramBareClient -from . import helpers, utils +from . import helpers, utils, events from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, @@ -1760,26 +1761,17 @@ class TelegramClient(TelegramBareClient): def on(self, event): """ - - Turns the given entity into a valid Telegram user or chat. + Decorator helper method around add_event_handler(). Args: event (:obj:`_EventBuilder` | :obj:`type`): The event builder class or instance to be used, for instance ``events.NewMessage``. """ - if isinstance(event, type): - event = event() - - event.resolve(self) - def decorator(f): - self._event_builders.append((event, f)) + self.add_event_handler(f, event) return f - if self._on_handler not in self.updates.handlers: - self.add_update_handler(self._on_handler) - return decorator def _on_handler(self, update): @@ -1789,6 +1781,49 @@ class TelegramClient(TelegramBareClient): event._client = self callback(event) + def add_event_handler(self, callback, event): + """ + Registers the given callback to be called on the specified event. + + Args: + callback (:obj:`callable`): + The callable function accepting one parameter to be used. + + event (:obj:`_EventBuilder` | :obj:`type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + """ + if self.updates.workers is None: + warnings.warn( + "You have not setup any workers, so you won't receive updates." + " Pass update_workers=1 when creating the TelegramClient," + " or set client.self.updates.workers = 1" + ) + + self.updates.handler = self._on_handler + if isinstance(event, type): + event = event() + + event.resolve(self) + self._event_builders.append((event, callback)) + + 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""" + warnings.warn( + 'add_update_handler is deprecated, use the @client.on syntax ' + 'or add_event_handler(callback, events.Raw) instead (see ' + 'https://telethon.rtfd.io/en/latest/extra/basic/working-' + 'with-updates.html)' + ) + self.add_event_handler(handler, events.Raw) + + def remove_update_handler(self, handler): + pass + + def list_update_handlers(self): + return [] + # endregion # region Small utilities to make users' life easier diff --git a/telethon/update_state.py b/telethon/update_state.py index f98c0c04..6fa0b12a 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -22,12 +22,12 @@ class UpdateState: workers is None: Updates will *not* be stored on self. workers = 0: Another thread is responsible for calling self.poll() workers > 0: 'workers' background threads will be spawned, any - any of them will invoke all the self.handlers. + any of them will invoke the self.handler. """ self._workers = workers self._worker_threads = [] - self.handlers = [] + self.handler = None self._updates_lock = RLock() self._updates = Queue() @@ -106,10 +106,8 @@ class UpdateState: while True: try: update = self.poll(timeout=UpdateState.WORKER_POLL_TIMEOUT) - # TODO Maybe people can add different handlers per update type - if update: - for handler in self.handlers: - handler(update) + if update and self.handler: + self.handler(update) except StopIteration: break except: From 89df481ae4ebba62c1e563729a4f87bc493aaee1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Feb 2018 14:07:13 +0100 Subject: [PATCH 191/361] Make MessageChanged.Event inherit NewMessage.Event --- telethon/events/__init__.py | 74 +++++-------------------------------- 1 file changed, 9 insertions(+), 65 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 91683594..db08a04c 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -814,86 +814,30 @@ class MessageChanged(_EventBuilder): return self._filter_event(event) - class Event(_EventCommon): + class Event(NewMessage.Event): """ Represents the event of an user status update (last seen, joined). + Please note that the ``message`` member will be ``None`` if the + action was a deletion and not an edit. + Members: edited (:obj:`bool`): ``True`` if the message was edited. - message (:obj:`Message`, optional): - The new edited message, if any. - deleted (:obj:`bool`): ``True`` if the message IDs were deleted. deleted_ids (:obj:`List[int]`): A list containing the IDs of the messages that were deleted. - - input_sender (:obj:`InputPeer`): - This is the input version of the user who edited the message. - Similarly to ``input_chat``, this doesn't have things like - username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat. - - sender (:obj:`User`): - This property will make an API call the first time to get the - most up to date version of the sender, so use with care as - there is no caching besides local caching yet. - - ``input_sender`` needs to be available (often the case). """ def __init__(self, edit_msg=None, deleted_ids=None, peer=None): - super().__init__(peer if not edit_msg else edit_msg.to_id) + if edit_msg is None: + msg = types.Message((deleted_ids or [0])[0], peer, None, '') + else: + msg = edit_msg + super().__init__(msg) self.edited = bool(edit_msg) - self.message = edit_msg self.deleted = bool(deleted_ids) self.deleted_ids = deleted_ids or [] - self._input_sender = None - self._sender = None - - @property - def input_sender(self): - """ - This (:obj:`InputPeer`) is the input version of the user who - sent the message. Similarly to ``input_chat``, this doesn't have - things like username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat, or if the message a broadcast on a channel. - """ - # TODO Code duplication - if self._input_sender is None: - if self.is_channel and not self.is_group: - return None - - try: - self._input_sender = self._client.get_input_entity( - self.message.from_id - ) - except (ValueError, TypeError): - # We can rely on self.input_chat for this - self._input_sender = self._get_input_entity( - self.message.id, - self.message.from_id, - chat=self.input_chat - ) - - return self._input_sender - - @property - def sender(self): - """ - This (:obj:`User`) will make an API call the first time to get - the most up to date version of the sender, so use with care as - there is no caching besides local caching yet. - - ``input_sender`` needs to be available (often the case). - """ - if self._sender is None and self.input_sender: - self._sender = self._client.get_entity(self._input_sender) - return self._sender From b136074340c510af6421551b45ba120f5567352a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Feb 2018 14:23:11 +0100 Subject: [PATCH 192/361] Update to v0.17.3 --- readthedocs/extra/changelog.rst | 31 +++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 34609615..2fe0352f 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,37 @@ it can take advantage of new goodies! .. contents:: List of All Versions +New small convenience functions (v0.17.3) +========================================= + +*Published at 2018/02/18* + +More bug fixes and a few others addition to make events easier to use. + +Additions +~~~~~~~~~ + +- Use ``hachoir`` to extract video and audio metadata before upload. +- New ``.add_event_handler``, ``.add_update_handler`` now deprecated. + +Bug fixes +~~~~~~~~~ + +- ``bot_token`` wouldn't work on ``.start()``, and changes to ``password`` + (now it will ask you for it if you don't provide it, as docstring hinted). +- ``.edit_message()`` was ignoring the formatting (e.g. markdown). +- Added missing case to the ``NewMessage`` event for normal groups. +- Accessing the ``.text`` of the ``NewMessage`` event was failing due + to a bug with the markdown unparser. + +Internal changes +~~~~~~~~~~~~~~~~ + +- ``libssl`` is no longer an optional dependency. Use ``cryptg`` instead, + which you can find on https://github.com/Lonami/cryptg. + + + New small convenience functions (v0.17.2) ========================================= diff --git a/telethon/version.py b/telethon/version.py index 4ead720b..eaeb252f 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.17.2' +__version__ = '0.17.3' From 1eeedc613b406d1c27cc5a0e28c6e23efd5cf552 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 15:29:32 +0100 Subject: [PATCH 193/361] Fix sending byte strings as files not working --- telethon/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index 2f27b79e..643fa746 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -323,17 +323,19 @@ def get_input_media(media, user_caption=None, is_photo=False): def is_image(file): """Returns True if the file extension looks like an image file""" - return (mimetypes.guess_type(file)[0] or '').startswith('image/') - + return (isinstance(file, str) and + (mimetypes.guess_type(file)[0] or '').startswith('image/')) def is_audio(file): """Returns True if the file extension looks like an audio file""" - return (mimetypes.guess_type(file)[0] or '').startswith('audio/') + return (isinstance(file, str) and + (mimetypes.guess_type(file)[0] or '').startswith('audio/')) def is_video(file): """Returns True if the file extension looks like a video file""" - return (mimetypes.guess_type(file)[0] or '').startswith('video/') + return (isinstance(file, str) and + (mimetypes.guess_type(file)[0] or '').startswith('video/')) def parse_phone(phone): From bf086f3e801fc1bd38cb24004e4ad3fb01db183f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 20:23:52 +0100 Subject: [PATCH 194/361] Fix UpdateDeleteMessages doesn't have .channel_id (#619) --- telethon/events/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index db08a04c..9274813e 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -803,8 +803,12 @@ class MessageChanged(_EventBuilder): if isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): event = MessageChanged.Event(edit_msg=update.message) - elif isinstance(update, (types.UpdateDeleteMessages, - types.UpdateDeleteChannelMessages)): + elif isinstance(update, types.UpdateDeleteMessages): + event = MessageChanged.Event( + deleted_ids=update.messages, + peer=None + ) + elif isinstance(update, types.UpdateDeleteChannelMessages): event = MessageChanged.Event( deleted_ids=update.messages, peer=types.PeerChannel(update.channel_id) From 39621ceae90a5ad0602ed8d74a2a5eabae09d37c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 20:31:47 +0100 Subject: [PATCH 195/361] Use req_pq_multi instead req_pq when creating an auth_key --- telethon/network/authenticator.py | 4 ++-- telethon_generator/scheme.tl | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index a73bae38..32413551 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -17,7 +17,7 @@ from ..errors import SecurityError from ..extensions import BinaryReader from ..network import MtProtoPlainSender from ..tl.functions import ( - ReqPqRequest, ReqDHParamsRequest, SetClientDHParamsRequest + ReqPqMultiRequest, ReqDHParamsRequest, SetClientDHParamsRequest ) @@ -53,7 +53,7 @@ def _do_authentication(connection): sender = MtProtoPlainSender(connection) # Step 1 sending: PQ Request, endianness doesn't matter since it's random - req_pq_request = ReqPqRequest( + req_pq_request = ReqPqMultiRequest( nonce=int.from_bytes(os.urandom(16), 'big', signed=True) ) sender.send(bytes(req_pq_request)) diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index 1d03c281..491f0c9e 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -53,7 +53,10 @@ destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes; ---functions--- +// Deprecated since somewhere around February of 2018 +// See https://core.telegram.org/mtproto/auth_key req_pq#60469778 nonce:int128 = ResPQ; +req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params; From c31635cc34e97f9ac1be6f769a4a1060a866503a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 21:03:33 +0100 Subject: [PATCH 196/361] Further validate the username on parse_username --- telethon/utils.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index 643fa746..607897e4 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -30,6 +30,8 @@ USERNAME_RE = re.compile( r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' ) +VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$') + def get_display_name(entity): """Gets the input peer for the given "entity" (user, chat or channel) @@ -326,6 +328,7 @@ def is_image(file): return (isinstance(file, str) and (mimetypes.guess_type(file)[0] or '').startswith('image/')) + def is_audio(file): """Returns True if the file extension looks like an audio file""" return (isinstance(file, str) and @@ -353,15 +356,21 @@ def parse_username(username): a string, username or URL. Returns a tuple consisting of both the stripped, lowercase username and whether it is a joinchat/ hash (in which case is not lowercase'd). + + Returns None if the username is not valid. """ username = username.strip() m = USERNAME_RE.match(username) if m: - result = username[m.end():] + username = username[m.end():] is_invite = bool(m.group(1)) - return result if is_invite else result.lower(), is_invite - else: + if is_invite: + return username, True + + if VALID_USERNAME_RE.match(username): return username.lower(), False + else: + return None, False def get_peer_id(peer): From 4050d1ca008eeb1cfcd1480eafa1c47a811b6b46 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Feb 2018 21:04:41 +0100 Subject: [PATCH 197/361] Support getting entities by exact name/title match --- telethon/session.py | 10 ++++++++-- telethon/telegram_client.py | 17 +++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/telethon/session.py b/telethon/session.py index e168e559..16232b14 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -438,13 +438,19 @@ class Session: (phone,)) else: username, _ = utils.parse_username(key) - c.execute('select id, hash from entities where username=?', - (username,)) + if username: + c.execute('select id, hash from entities where username=?', + (username,)) if isinstance(key, int): c.execute('select id, hash from entities where id=?', (key,)) result = c.fetchone() + if not result and isinstance(key, str): + # Try exact match by name if phone/username failed + c.execute('select id, hash from entities where name=?', (key,)) + result = c.fetchone() + c.close() if result: i, h = result # unpack resulting tuple diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 118f1f53..10dbb8c5 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1915,9 +1915,9 @@ class TelegramClient(TelegramBareClient): if user.phone == phone: return user else: - string, is_join_chat = utils.parse_username(string) + username, is_join_chat = utils.parse_username(string) if is_join_chat: - invite = self(CheckChatInviteRequest(string)) + invite = self(CheckChatInviteRequest(username)) if isinstance(invite, ChatInvite): raise ValueError( 'Cannot get entity from a channel ' @@ -1925,13 +1925,18 @@ class TelegramClient(TelegramBareClient): ) elif isinstance(invite, ChatInviteAlready): return invite.chat - else: - if string in ('me', 'self'): + elif username: + if username in ('me', 'self'): return self.get_me() - result = self(ResolveUsernameRequest(string)) + result = self(ResolveUsernameRequest(username)) for entity in itertools.chain(result.users, result.chats): - if entity.username.lower() == string: + if entity.username.lower() == username: return entity + try: + # Nobody with this username, maybe it's an exact name/title + return self.get_entity(self.get_input_entity(string)) + except (ValueError, TypeError): + pass raise TypeError( 'Cannot turn "{}" into any entity (user or chat)'.format(string) From ea0da8fc0e58c78c92339af41bba5e660b824fbe Mon Sep 17 00:00:00 2001 From: Jannik <32801117+code1mountain@users.noreply.github.com> Date: Tue, 20 Feb 2018 15:55:02 +0100 Subject: [PATCH 198/361] Add pattern argument on the NewMessage event (#620) --- telethon/events/__init__.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 9274813e..99dd5bda 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,6 +1,7 @@ import abc import datetime import itertools +import re from .. import utils from ..errors import RPCError @@ -173,19 +174,33 @@ class NewMessage(_EventBuilder): If set to ``True``, only **outgoing** messages will be handled. Mutually exclusive with ``incoming`` (can only set one of either). + pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional): + If set, only messages matching this pattern will be handled. + You can specify a regex-like string which will be matched + against the message, a callable function that returns ``True`` + if a message is acceptable, or a compiled regex pattern. + Notes: The ``message.from_id`` might not only be an integer or ``None``, but also ``InputPeerSelf()`` for short private messages (the API would not return such thing, this is a custom modification). """ def __init__(self, incoming=None, outgoing=None, - chats=None, blacklist_chats=False): + chats=None, blacklist_chats=False, pattern=None): if incoming and outgoing: raise ValueError('Can only set either incoming or outgoing') super().__init__(chats=chats, blacklist_chats=blacklist_chats) self.incoming = incoming self.outgoing = outgoing + if isinstance(pattern, str): + self.pattern = re.compile(pattern).match + elif not pattern or callable(pattern): + self.pattern = pattern + elif hasattr(pattern, 'match') and callable(pattern.match): + self.pattern = pattern.match + else: + raise TypeError('Invalid pattern type given') def build(self, update): if isinstance(update, @@ -229,13 +244,16 @@ class NewMessage(_EventBuilder): return # Short-circuit if we let pass all events - if all(x is None for x in (self.incoming, self.outgoing, self.chats)): + if all(x is None for x in (self.incoming, self.outgoing, self.chats, + self.pattern)): return event if self.incoming and event.message.out: return if self.outgoing and not event.message.out: return + if self.pattern and not self.pattern(event.message.message or ''): + return return self._filter_event(event) From 0731a1d6983e663f8cb5cf282641c702027ae21c Mon Sep 17 00:00:00 2001 From: Dmitry Bukhta Date: Tue, 20 Feb 2018 17:58:51 +0300 Subject: [PATCH 199/361] Raise ProxyConnectionError instead looping forever (#621) We shouldn't try reconnecting when using a proxy if what's unavailable is the proxy server (and not Telegram servers). --- telethon/extensions/tcp_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index a306302a..dd177aa2 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -8,6 +8,11 @@ from datetime import timedelta from io import BytesIO, BufferedWriter from threading import Lock +try: + import socks +except ImportError: + socks = None + MAX_TIMEOUT = 15 # in seconds CONN_RESET_ERRNOS = { errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH, @@ -70,6 +75,9 @@ class TcpClient: self._socket.connect(address) break # Successful connection, stop retrying to connect except OSError as e: + # Stop retrying to connect if proxy connection error occurred + if socks and isinstance(e, socks.ProxyConnectionError): + raise # There are some errors that we know how to handle, and # the loop will allow us to retry if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL, From 7f35ed59c613f7b283dae639ec93842c4115cb87 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 20 Feb 2018 17:30:01 +0100 Subject: [PATCH 200/361] Fix infinite recursion on .get_entity by exact name --- 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 10dbb8c5..cd791964 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1934,8 +1934,8 @@ class TelegramClient(TelegramBareClient): return entity try: # Nobody with this username, maybe it's an exact name/title - return self.get_entity(self.get_input_entity(string)) - except (ValueError, TypeError): + return self.get_entity(self.session.get_input_entity(string)) + except ValueError: pass raise TypeError( From 359cdcd77282b73c9a1f844dde2a718e06cc8cad Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Feb 2018 10:27:12 +0100 Subject: [PATCH 201/361] Handle more parsing username cases (closes #630) --- telethon/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telethon/utils.py b/telethon/utils.py index 607897e4..fdadbd1c 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -27,7 +27,7 @@ from .tl.types import ( from .tl.types.contacts import ResolvedPeer USERNAME_RE = re.compile( - r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' + r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' ) VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$') @@ -366,6 +366,8 @@ def parse_username(username): is_invite = bool(m.group(1)) if is_invite: return username, True + else: + username = username.rstrip('/') if VALID_USERNAME_RE.match(username): return username.lower(), False From f13a7e4afde5340fd72528b9319d7547127b1ccc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Feb 2018 20:37:56 +0100 Subject: [PATCH 202/361] Allow getting the input peer for yourself and cache it Warm-up for #632, which needs this information accessible. --- telethon/telegram_client.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index cd791964..4b9bda67 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -179,6 +179,9 @@ class TelegramClient(TelegramBareClient): self._phone_code_hash = {} self._phone = None + # Sometimes we need to know who we are, cache the self peer + self._self_input_peer = None + # endregion # region Telegram requests functions @@ -407,6 +410,9 @@ class TelegramClient(TelegramBareClient): 'and a password only if an RPCError was raised before.' ) + self._self_input_peer = utils.get_input_peer( + result.user, allow_self=False + ) self._set_connected_and_authorized() return result.user @@ -436,6 +442,9 @@ class TelegramClient(TelegramBareClient): last_name=last_name )) + self._self_input_peer = utils.get_input_peer( + result.user, allow_self=False + ) self._set_connected_and_authorized() return result.user @@ -455,16 +464,30 @@ class TelegramClient(TelegramBareClient): self.session.delete() return True - def get_me(self): + def get_me(self, input_peer=False): """ Gets "me" (the self user) which is currently authenticated, or None if the request fails (hence, not authenticated). + Args: + input_peer (:obj:`bool`, optional): + Whether to return the ``InputPeerUser`` version or the normal + ``User``. This can be useful if you just need to know the ID + of yourself. + Returns: :obj:`User`: Your own user. """ + if input_peer and self._self_input_peer: + return self._self_input_peer + try: - return self(GetUsersRequest([InputUserSelf()]))[0] + me = self(GetUsersRequest([InputUserSelf()]))[0] + if not self._self_input_peer: + self._self_input_peer = utils.get_input_peer( + me, allow_self=False + ) + return me except UnauthorizedError: return None From 448a04a7c5be07d28cfe63c8a16f2eab5762c7f0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Feb 2018 21:01:18 +0100 Subject: [PATCH 203/361] Stop using InputPeerSelf() on events and special case edit() Used to fail on the chat with your own (where messages are "incoming" instead outgoing). Now the ID of the chat and sender are compared to achieve the same effect. Fixes #632. --- telethon/events/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 99dd5bda..1d7beab2 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -21,8 +21,7 @@ def _into_id_set(client, chats): for chat in chats: chat = client.get_input_entity(chat) if isinstance(chat, types.InputPeerSelf): - chat = getattr(_into_id_set, 'me', None) or client.get_me() - _into_id_set.me = chat + chat = client.get_me(input_peer=True) result.add(utils.get_peer_id(chat)) return result @@ -43,6 +42,7 @@ class _EventBuilder(abc.ABC): def __init__(self, chats=None, blacklist_chats=False): self.chats = chats self.blacklist_chats = blacklist_chats + self._self_id = None @abc.abstractmethod def build(self, update): @@ -51,6 +51,7 @@ class _EventBuilder(abc.ABC): def resolve(self, client): """Helper method to allow event builders to be resolved before usage""" self.chats = _into_id_set(client, self.chats) + self._self_id = client.get_me(input_peer=True).user_id def _filter_event(self, event): """ @@ -179,11 +180,6 @@ class NewMessage(_EventBuilder): You can specify a regex-like string which will be matched against the message, a callable function that returns ``True`` if a message is acceptable, or a compiled regex pattern. - - Notes: - The ``message.from_id`` might not only be an integer or ``None``, - but also ``InputPeerSelf()`` for short private messages (the API - would not return such thing, this is a custom modification). """ def __init__(self, incoming=None, outgoing=None, chats=None, blacklist_chats=False, pattern=None): @@ -216,7 +212,7 @@ class NewMessage(_EventBuilder): silent=update.silent, id=update.id, to_id=types.PeerUser(update.user_id), - from_id=types.InputPeerSelf() if update.out else update.user_id, + from_id=self._self_id if update.out else update.user_id, message=update.message, date=update.date, fwd_from=update.fwd_from, @@ -317,7 +313,11 @@ class NewMessage(_EventBuilder): or the edited message otherwise. """ if not self.message.out: - return None + if not isinstance(self.message.to_id, types.PeerUser): + return None + me = self._client.get_me(input_peer=True) + if self.message.to_id.user_id != me.user_id: + return None return self._client.edit_message(self.input_chat, self.message, From cda5e59e86c6fd00c83ecd70820baa8172dd1a28 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Feb 2018 12:07:57 +0100 Subject: [PATCH 204/361] Make .send_message() accept another Message as input --- telethon/telegram_client.py | 43 +++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 4b9bda67..a5067e6c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -80,7 +80,8 @@ from .tl.types import ( InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, InputMessageEntityMentionName, DocumentAttributeVideo, - UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates + UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, + MessageMediaWebPage ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -664,8 +665,8 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): To who will it be sent. - message (:obj:`str`): - The message to be sent. + message (:obj:`str` | :obj:`Message`): + The message to be sent, or another message object to resend. reply_to (:obj:`int` | :obj:`Message`, optional): Whether to reply to a message or not. If an integer is provided, @@ -684,15 +685,35 @@ class TelegramClient(TelegramBareClient): the sent message """ entity = self.get_input_entity(entity) - message, msg_entities = self._parse_message_text(message, parse_mode) + if isinstance(message, Message): + if (message.media + and not isinstance(message.media, MessageMediaWebPage)): + return self.send_file(entity, message.media) + + if utils.get_peer_id(entity) == utils.get_peer_id(message.to_id): + reply_id = message.reply_to_msg_id + else: + reply_id = None + request = SendMessageRequest( + peer=entity, + message=message.message or '', + silent=message.silent, + reply_to_msg_id=reply_id, + reply_markup=message.reply_markup, + entities=message.entities, + no_webpage=not isinstance(message.media, MessageMediaWebPage) + ) + message = message.message + else: + message, msg_ent = self._parse_message_text(message, parse_mode) + request = SendMessageRequest( + peer=entity, + message=message, + entities=msg_ent, + no_webpage=not link_preview, + reply_to_msg_id=self._get_message_id(reply_to) + ) - request = SendMessageRequest( - peer=entity, - message=message, - entities=msg_entities, - no_webpage=not link_preview, - reply_to_msg_id=self._get_message_id(reply_to) - ) result = self(request) if isinstance(result, UpdateShortSentMessage): return Message( From 005a8f0a7f61efe2be0396d7bc3c77cc8126e5e6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Feb 2018 12:10:07 +0100 Subject: [PATCH 205/361] Fix .send_file() not respecting MessageMedia captions --- telethon/telegram_client.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a5067e6c..84704722 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1018,7 +1018,7 @@ class TelegramClient(TelegramBareClient): # region Uploading files - def send_file(self, entity, file, caption='', + def send_file(self, entity, file, caption=None, force_document=False, progress_callback=None, reply_to=None, attributes=None, @@ -1130,11 +1130,11 @@ class TelegramClient(TelegramBareClient): if isinstance(file_handle, use_cache): # File was cached, so an instance of use_cache was returned if as_image: - media = InputMediaPhoto(file_handle, caption) + media = InputMediaPhoto(file_handle, caption or '') else: - media = InputMediaDocument(file_handle, caption) + media = InputMediaDocument(file_handle, caption or '') elif as_image: - media = InputMediaUploadedPhoto(file_handle, caption) + media = InputMediaUploadedPhoto(file_handle, caption or '') else: mime_type = None if isinstance(file, str): @@ -1204,7 +1204,7 @@ class TelegramClient(TelegramBareClient): file=file_handle, mime_type=mime_type, attributes=list(attr_dict.values()), - caption=caption, + caption=caption or '', **input_kw ) @@ -1224,15 +1224,15 @@ class TelegramClient(TelegramBareClient): return msg - def send_voice_note(self, entity, file, caption='', progress_callback=None, - reply_to=None): + def send_voice_note(self, entity, file, caption=None, + progress_callback=None, reply_to=None): """Wrapper method around .send_file() with is_voice_note=()""" return self.send_file(entity, file, caption, progress_callback=progress_callback, reply_to=reply_to, is_voice_note=()) # empty tuple is enough - def _send_album(self, entity, files, caption='', + def _send_album(self, entity, files, caption=None, progress_callback=None, reply_to=None): """Specialized version of .send_file for albums""" # We don't care if the user wants to avoid cache, we will use it @@ -1241,6 +1241,7 @@ class TelegramClient(TelegramBareClient): # cache only makes a difference for documents where the user may # want the attributes used on them to change. Caption's ignored. entity = self.get_input_entity(entity) + caption = caption or '' reply_to = self._get_message_id(reply_to) # Need to upload the media first, but only if they're not cached yet From a353679796891ef8e0443233bf44c145d1376611 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Feb 2018 13:13:39 +0100 Subject: [PATCH 206/361] Fix downloading from another DC using wrong auth the first time --- telethon/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/session.py b/telethon/session.py index 16232b14..4658df2b 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -227,7 +227,7 @@ class Session: c = self._cursor() c.execute('select auth_key from sessions') tuple_ = c.fetchone() - if tuple_: + if tuple_ and tuple_[0]: self._auth_key = AuthKey(data=tuple_[0]) else: self._auth_key = None From f9cec54c3970aac823ecc96c553012ff059385ea Mon Sep 17 00:00:00 2001 From: Kyle2142 Date: Fri, 23 Feb 2018 22:20:32 +0200 Subject: [PATCH 207/361] Add .get_participants() convenience method (#639) Closes #363 and #380. --- telethon/telegram_client.py | 59 +++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 84704722..3e7918d2 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -56,7 +56,7 @@ from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, - UploadMediaRequest, EditMessageRequest + UploadMediaRequest, EditMessageRequest, GetFullChatRequest ) from .tl.functions import channels @@ -66,7 +66,7 @@ from .tl.functions.users import ( GetUsersRequest ) from .tl.functions.channels import ( - GetChannelsRequest, GetFullChannelRequest + GetChannelsRequest, GetFullChannelRequest, GetParticipantsRequest ) from .tl.types import ( DocumentAttributeAudio, DocumentAttributeFilename, @@ -81,7 +81,7 @@ from .tl.types import ( InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, InputMessageEntityMentionName, DocumentAttributeVideo, UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, - MessageMediaWebPage + MessageMediaWebPage, ChannelParticipantsSearch ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -1014,6 +1014,59 @@ class TelegramClient(TelegramBareClient): raise TypeError('Invalid message type: {}'.format(type(message))) + def get_participants(self, entity, limit=None, search=''): + """ + Gets the list of participants from the specified entity + + Args: + entity (:obj:`entity`): + The entity from which to retrieve the participants list. + + limit (:obj: `int`): + Limits amount of participants fetched. + + search (:obj: `str`, optional): + Look for participants with this string in name/username. + + Returns: + A list of participants with an additional .total variable on the list + indicating the total amount of members in this group/channel. + """ + entity = self.get_input_entity(entity) + limit = float('inf') if limit is None else int(limit) + if isinstance(entity, InputPeerChannel): + offset = 0 + all_participants = {} + search = ChannelParticipantsSearch(search) + while True: + loop_limit = min(limit - offset, 200) + participants = self(GetParticipantsRequest( + entity, search, offset, loop_limit, hash=0 + )) + if not participants.users: + break + for user in participants.users: + if len(all_participants) < limit: + all_participants[user.id] = user + offset += len(participants.users) + if offset > limit: + break + + users = UserList(all_participants.values()) + users.total = self(GetFullChannelRequest( + entity)).full_chat.participants_count + + elif isinstance(entity, InputPeerChat): + users = self(GetFullChatRequest(entity.chat_id)).users + if len(users) > limit: + users = users[:limit] + users = UserList(users) + users.total = len(users) + else: + users = UserList([entity]) + users.total = 1 + return users + # endregion # region Uploading files From b7a61510bf7c7903af50d96d6013977e79ef8a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Fri, 23 Feb 2018 21:34:15 +0100 Subject: [PATCH 208/361] Add !i for information to the interactive telegram client (#614) --- .../interactive_telegram_client.py | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index d45d2ff1..f6986370 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -1,12 +1,13 @@ import os from getpass import getpass -from telethon import TelegramClient, ConnectionMode +from telethon.utils import get_display_name + +from telethon import ConnectionMode, TelegramClient from telethon.errors import SessionPasswordNeededError from telethon.tl.types import ( - UpdateShortChatMessage, UpdateShortMessage, PeerChat + PeerChat, UpdateShortChatMessage, UpdateShortMessage ) -from telethon.utils import get_display_name def sprint(string, *args, **kwargs): @@ -47,6 +48,7 @@ class InteractiveTelegramClient(TelegramClient): Telegram through Telethon, such as listing dialogs (open chats), talking to people, downloading media, and receiving updates. """ + def __init__(self, session_user_id, user_phone, api_id, api_hash, proxy=None): """ @@ -182,14 +184,15 @@ class InteractiveTelegramClient(TelegramClient): # Show some information print_title('Chat with "{}"'.format(get_display_name(entity))) print('Available commands:') - print(' !q: Quits the current chat.') - print(' !Q: Quits the current chat and exits.') - print(' !h: prints the latest messages (message History).') - print(' !up : Uploads and sends the Photo from path.') - print(' !uf : Uploads and sends the File from path.') - print(' !d : Deletes a message by its id') - print(' !dm : Downloads the given message Media (if any).') + print(' !q: Quits the current chat.') + print(' !Q: Quits the current chat and exits.') + print(' !h: prints the latest messages (message History).') + print(' !up : Uploads and sends the Photo from path.') + print(' !uf : Uploads and sends the File from path.') + print(' !d : Deletes a message by its id') + print(' !dm : Downloads the given message Media (if any).') print(' !dp: Downloads the current dialog Profile picture.') + print(' !i: Prints information about this chat..') print() # And start a while loop to chat @@ -234,8 +237,7 @@ class InteractiveTelegramClient(TelegramClient): # And print it to the user sprint('[{}:{}] (ID={}) {}: {}'.format( - msg.date.hour, msg.date.minute, msg.id, name, - content)) + msg.date.hour, msg.date.minute, msg.id, name, content)) # Send photo elif msg.startswith('!up '): @@ -264,12 +266,16 @@ class InteractiveTelegramClient(TelegramClient): os.makedirs('usermedia', exist_ok=True) output = self.download_profile_photo(entity, 'usermedia') if output: - print( - 'Profile picture downloaded to {}'.format(output) - ) + print('Profile picture downloaded to', output) else: print('No profile picture found for this user!') + elif msg == '!i': + attributes = list(entity.to_dict().items()) + pad = max(len(x) for x, _ in attributes) + for name, val in attributes: + print("{:<{width}} : {}".format(name, val, width=pad)) + # Send chat message (if any) elif msg: self.send_message(entity, msg, link_preview=False) @@ -356,6 +362,5 @@ class InteractiveTelegramClient(TelegramClient): else: who = self.get_entity(update.from_id) sprint('<< {} @ {} sent "{}"'.format( - get_display_name(which), get_display_name(who), - update.message + get_display_name(which), get_display_name(who), update.message )) From 760d84514f5fc727501126de99d2ffb4e2b21052 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Sat, 24 Feb 2018 18:25:08 +1000 Subject: [PATCH 209/361] setup: Fix regex failure to match version in case of CRLF line feeds This could happen e.g. in case of using pip3 to install Telethon directly from the git repo. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 85e77a74..00dd7446 100755 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ def main(): long_description = f.read() with open('telethon/version.py', encoding='utf-8') as f: - version = re.search(r"^__version__\s+=\s+'(.*)'$", + version = re.search(r"^__version__\s*=\s*'(.*)'.*$", f.read(), flags=re.MULTILINE).group(1) setup( name='Telethon', From 7f97997e8def6b5cb5c6045d04ad916110934dd7 Mon Sep 17 00:00:00 2001 From: "Dmitry D. Chernov" Date: Sat, 24 Feb 2018 18:41:53 +1000 Subject: [PATCH 210/361] Add PySocks to the package optional requirements --- optional-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/optional-requirements.txt b/optional-requirements.txt index c973d402..55bfc014 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,2 +1,3 @@ cryptg +pysocks hachoir3 From 3301bf3ff6383d51ae8dbab411636428ee9569f9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Feb 2018 17:40:12 +0100 Subject: [PATCH 211/361] Fix voice notes default filename being "None - None.oga" --- telethon/telegram_client.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3e7918d2..3f341099 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1622,18 +1622,27 @@ class TelegramClient(TelegramBareClient): file_size = document.size + kind = 'document' possible_names = [] for attr in document.attributes: if isinstance(attr, DocumentAttributeFilename): possible_names.insert(0, attr.file_name) elif isinstance(attr, DocumentAttributeAudio): - possible_names.append('{} - {}'.format( - attr.performer, attr.title - )) + kind = 'audio' + if attr.performer and attr.title: + possible_names.append('{} - {}'.format( + attr.performer, attr.title + )) + elif attr.performer: + possible_names.append(attr.performer) + elif attr.title: + possible_names.append(attr.title) + elif attr.voice: + kind = 'voice' file = self._get_proper_filename( - file, 'document', utils.get_extension(document), + file, kind, utils.get_extension(document), date=date, possible_names=possible_names ) From e5aecca79ce77c6643cd87857918cf77ac6fb781 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Feb 2018 18:08:14 +0100 Subject: [PATCH 212/361] Update to v0.17.4 --- readthedocs/extra/changelog.rst | 39 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 2fe0352f..ad027361 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,45 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Further easing library usage (v0.17.4) +====================================== + +*Published at 2018/02/24* + +Some new things and patches that already deserved their own release. + + +Additions +~~~~~~~~~ + +- New ``pattern`` argument to ``NewMessage`` to easily filter messages. +- New ``.get_participants()`` convenience method to get members from chats. +- ``.send_message()`` now accepts a ``Message`` as the ``message`` parameter. +- You can now ``.get_entity()`` through exact name match instead username. +- Raise ``ProxyConnectionError`` instead looping forever so you can + ``except`` it on your own code and behave accordingly. + +Bug fixes +~~~~~~~~~ + +- ``.parse_username`` would fail with ``www.`` or a trailing slash. +- ``events.MessageChanged`` would fail with ``UpdateDeleteMessages``. +- You can now send ``b'byte strings'`` directly as files again. +- ``.send_file()`` was not respecting the original captions when passing + another message (or media) as the file. +- Downloading media from a different data center would always log a warning + for the first time. + +Internal changes +~~~~~~~~~~~~~~~~ + +- Use ``req_pq_multi`` instead ``req_pq`` when generating ``auth_key``. +- You can use ``.get_me(input_peer=True)`` if all you need is your self ID. +- New addition to the interactive client example to show peer information. +- Avoid special casing ``InputPeerSelf`` on some ``NewMessage`` events, so + you can always safely rely on ``.sender`` to get the right ID. + + New small convenience functions (v0.17.3) ========================================= diff --git a/telethon/version.py b/telethon/version.py index eaeb252f..8cc14b33 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.17.3' +__version__ = '0.17.4' From 9ef75e5070bb93dd73fb35d1d33f82688ee6a2db Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Feb 2018 18:25:22 +0100 Subject: [PATCH 213/361] Allow specifying no event type to default to events.Raw --- telethon/telegram_client.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3f341099..55babafe 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1888,7 +1888,7 @@ class TelegramClient(TelegramBareClient): event._client = self callback(event) - def add_event_handler(self, callback, event): + def add_event_handler(self, callback, event=None): """ Registers the given callback to be called on the specified event. @@ -1896,9 +1896,12 @@ class TelegramClient(TelegramBareClient): callback (:obj:`callable`): The callable function accepting one parameter to be used. - event (:obj:`_EventBuilder` | :obj:`type`): + event (:obj:`_EventBuilder` | :obj:`type`, optional): The event builder class or instance to be used, for instance ``events.NewMessage``. + + If left unspecified, ``events.Raw`` (the ``Update`` objects + with no further processing) will be passed instead. """ if self.updates.workers is None: warnings.warn( @@ -1910,6 +1913,8 @@ class TelegramClient(TelegramBareClient): self.updates.handler = self._on_handler if isinstance(event, type): event = event() + elif not event: + event = events.Raw() event.resolve(self) self._event_builders.append((event, callback)) From cfc5ecfdedefdb0331301cd2b652090ee204e695 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Feb 2018 18:30:09 +0100 Subject: [PATCH 214/361] Fix tiny bug regarding .get_me(input_peer=True) crashing events --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 55babafe..1e418904 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -488,7 +488,8 @@ class TelegramClient(TelegramBareClient): self._self_input_peer = utils.get_input_peer( me, allow_self=False ) - return me + + return self._self_input_peer if input_peer else me except UnauthorizedError: return None From 098602ca13edd5013deb1bf9bd1d18b87be5c9ee Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 25 Feb 2018 10:36:53 +0100 Subject: [PATCH 215/361] Let events.Raw.resolve() be a no-op --- telethon/events/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 1d7beab2..204609c0 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -155,6 +155,9 @@ class Raw(_EventBuilder): """ Represents a raw event. The event is the update itself. """ + def resolve(self, client): + pass + def build(self, update): return update From 623c1bd7d178c2876d3dabada4095ff4dd0cfdbc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 25 Feb 2018 20:34:40 +0100 Subject: [PATCH 216/361] Add missing parameters to TelegramClient.send_voice_note --- telethon/telegram_client.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 1e418904..562a6d7e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1278,13 +1278,9 @@ class TelegramClient(TelegramBareClient): return msg - def send_voice_note(self, entity, file, caption=None, - progress_callback=None, reply_to=None): - """Wrapper method around .send_file() with is_voice_note=()""" - return self.send_file(entity, file, caption, - progress_callback=progress_callback, - reply_to=reply_to, - is_voice_note=()) # empty tuple is enough + def send_voice_note(self, *args, **kwargs): + """Wrapper method around .send_file() with is_voice_note=True""" + return self.send_file(*args, **kwargs, is_voice_note=True) def _send_album(self, entity, files, caption=None, progress_callback=None, reply_to=None): From 3b0ab7794b7399f126a0088d8d3ad9e3b5ec1118 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 25 Feb 2018 20:35:55 +0100 Subject: [PATCH 217/361] Get name attribute from streams instead always 'unnamed' --- 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 562a6d7e..a2bf5e85 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1226,8 +1226,8 @@ class TelegramClient(TelegramBareClient): attr_dict[DocumentAttributeVideo] = doc else: attr_dict = { - DocumentAttributeFilename: - DocumentAttributeFilename('unnamed') + DocumentAttributeFilename: DocumentAttributeFilename( + getattr(file, 'name', None) or 'unnamed') } if 'is_voice_note' in kwargs: From 9604161c91b18169ba6de6e02d349d2091d8c29b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 26 Feb 2018 12:14:21 +0100 Subject: [PATCH 218/361] Fix incoming private messages not working with whitelists For some reason this was only happening with bots and not actual private messages. The fix doesn't seem to affect previous behaviour with actual users in private messages. --- telethon/events/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 204609c0..1f3b15f2 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -277,7 +277,14 @@ class NewMessage(_EventBuilder): Whether the message is a reply to some other or not. """ def __init__(self, message): - super().__init__(chat_peer=message.to_id, + if not message.out and isinstance(message.to_id, types.PeerUser): + # Incoming message (e.g. from a bot) has to_id=us, and + # from_id=bot (the actual "chat" from an user's perspective). + chat_peer = types.PeerUser(message.from_id) + else: + chat_peer = message.to_id + + super().__init__(chat_peer=chat_peer, msg_id=message.id, broadcast=bool(message.post)) self.message = message From 6f16aeb55340ab1442efe9cd2b468a6abc85a69c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 26 Feb 2018 13:41:07 +0100 Subject: [PATCH 219/361] Add logging calls on the TcpClient --- telethon/extensions/tcp_client.py | 35 +++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index dd177aa2..d9cea2f0 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -2,6 +2,7 @@ This module holds a rough implementation of the C# TCP client. """ import errno +import logging import socket import time from datetime import timedelta @@ -19,6 +20,8 @@ CONN_RESET_ERRNOS = { errno.EINVAL, errno.ENOTCONN } +__log__ = logging.getLogger(__name__) + class TcpClient: """A simple TCP client to ease the work with sockets and proxies.""" @@ -75,6 +78,7 @@ class TcpClient: self._socket.connect(address) break # Successful connection, stop retrying to connect except OSError as e: + __log__.info('OSError "%s" raised while connecting', e) # Stop retrying to connect if proxy connection error occurred if socks and isinstance(e, socks.ProxyConnectionError): raise @@ -120,19 +124,22 @@ class TcpClient: :param data: the data to send. """ if self._socket is None: - self._raise_connection_reset() + self._raise_connection_reset(None) # TODO Timeout may be an issue when sending the data, Changed in v3.5: # The socket timeout is now the maximum total duration to send all data. try: self._socket.sendall(data) except socket.timeout as e: + __log__.debug('socket.timeout "%s" while writing data', e) raise TimeoutError() from e - except ConnectionError: - self._raise_connection_reset() + except ConnectionError as e: + __log__.info('ConnectionError "%s" while writing data', e) + self._raise_connection_reset(e) except OSError as e: + __log__.info('OSError "%s" while writing data', e) if e.errno in CONN_RESET_ERRNOS: - self._raise_connection_reset() + self._raise_connection_reset(e) else: raise @@ -144,7 +151,7 @@ class TcpClient: :return: the read data with len(data) == size. """ if self._socket is None: - self._raise_connection_reset() + self._raise_connection_reset(None) # TODO Remove the timeout from this method, always use previous one with BufferedWriter(BytesIO(), buffer_size=size) as buffer: @@ -153,17 +160,22 @@ class TcpClient: try: partial = self._socket.recv(bytes_left) except socket.timeout as e: + # These are somewhat common if the server has nothing + # to send to us, so use a lower logging priority. + __log__.debug('socket.timeout "%s" while reading data', e) raise TimeoutError() from e - except ConnectionError: - self._raise_connection_reset() + except ConnectionError as e: + __log__.info('ConnectionError "%s" while reading data', e) + self._raise_connection_reset(e) except OSError as e: + __log__.info('OSError "%s" while reading data', e) if e.errno in CONN_RESET_ERRNOS: - self._raise_connection_reset() + self._raise_connection_reset(e) else: raise if len(partial) == 0: - self._raise_connection_reset() + self._raise_connection_reset(None) buffer.write(partial) bytes_left -= len(partial) @@ -172,7 +184,8 @@ class TcpClient: buffer.flush() return buffer.raw.getvalue() - def _raise_connection_reset(self): + def _raise_connection_reset(self, original): """Disconnects the client and raises ConnectionResetError.""" self.close() # Connection reset -> flag as socket closed - raise ConnectionResetError('The server has closed the connection.') + raise ConnectionResetError('The server has closed the connection.')\ + from original From 5a54e2279fecc3bbfd44694ad68a8fdb52593a6a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 26 Feb 2018 14:12:21 +0100 Subject: [PATCH 220/361] Avoid relying on .__iter__ to tell iterators apart .send_file() would fail with stream objects (those from open()) since they are iterable, and asserting that they weren't bytes or str was not enough. --- telethon/events/__init__.py | 2 +- telethon/session.py | 6 +++--- telethon/telegram_client.py | 6 +++--- telethon/utils.py | 12 ++++++++++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 1f3b15f2..48b26004 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -14,7 +14,7 @@ def _into_id_set(client, chats): if chats is None: return None - if not hasattr(chats, '__iter__') or isinstance(chats, str): + if not utils.is_list_like(chats): chats = (chats,) result = set() diff --git a/telethon/session.py b/telethon/session.py index 4658df2b..faa1516f 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -355,14 +355,14 @@ class Session: if not self.save_entities: return - if not isinstance(tlo, TLObject) and hasattr(tlo, '__iter__'): + if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): # This may be a list of users already for instance entities = tlo else: entities = [] - if hasattr(tlo, 'chats') and hasattr(tlo.chats, '__iter__'): + if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats): entities.extend(tlo.chats) - if hasattr(tlo, 'users') and hasattr(tlo.users, '__iter__'): + if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): entities.extend(tlo.users) if not entities: return diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a2bf5e85..1dad2716 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -975,7 +975,7 @@ class TelegramClient(TelegramBareClient): """ if max_id is None: if message: - if hasattr(message, '__iter__'): + if utils.is_list_like(message): max_id = max(msg.id for msg in message) else: max_id = message.id @@ -1140,7 +1140,7 @@ class TelegramClient(TelegramBareClient): """ # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. - if hasattr(file, '__iter__') and not isinstance(file, (str, bytes)): + if utils.is_list_like(file): # Convert to tuple so we can iterate several times file = tuple(x for x in file) if all(utils.is_image(x) for x in file): @@ -1960,7 +1960,7 @@ class TelegramClient(TelegramBareClient): ``User``, ``Chat`` or ``Channel`` corresponding to the input entity. """ - if hasattr(entity, '__iter__') and not isinstance(entity, str): + if utils.is_list_like(entity): single = False else: single = True diff --git a/telethon/utils.py b/telethon/utils.py index fdadbd1c..8f38563a 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -5,6 +5,7 @@ to convert between an entity like an User, Chat, etc. into its Input version) import math import mimetypes import re +import types from mimetypes import add_type, guess_extension from .tl.types import ( @@ -341,6 +342,17 @@ def is_video(file): (mimetypes.guess_type(file)[0] or '').startswith('video/')) +def is_list_like(obj): + """ + Returns True if the given object looks like a list. + + Checking if hasattr(obj, '__iter__') and ignoring str/bytes is not + enough. Things like open() are also iterable (and probably many + other things), so just support the commonly known list-like objects. + """ + return isinstance(obj, (list, tuple, set, dict, types.GeneratorType)) + + def parse_phone(phone): """Parses the given phone, or returns None if it's invalid""" if isinstance(phone, int): From 8d1b6629cb7e68ac14737ec533aa48475fa71efc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 26 Feb 2018 14:14:44 +0100 Subject: [PATCH 221/361] Sending open()'ed files would make their name the entire path --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 1dad2716..158855ad 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1227,7 +1227,8 @@ class TelegramClient(TelegramBareClient): else: attr_dict = { DocumentAttributeFilename: DocumentAttributeFilename( - getattr(file, 'name', None) or 'unnamed') + os.path.basename( + getattr(file, 'name', None) or 'unnamed')) } if 'is_voice_note' in kwargs: From 29f10f277179f89082e0e9bd43bb942b7d04da9e Mon Sep 17 00:00:00 2001 From: Lucas Yuji Suguinoshita Aciole Date: Tue, 27 Feb 2018 06:05:27 -0300 Subject: [PATCH 222/361] Fix named arguments after kwargs (#646) In Python3, you're unable to send named parameters after **kwargs * Use single quotes --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 158855ad..a8270aaf 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1281,7 +1281,8 @@ class TelegramClient(TelegramBareClient): def send_voice_note(self, *args, **kwargs): """Wrapper method around .send_file() with is_voice_note=True""" - return self.send_file(*args, **kwargs, is_voice_note=True) + kwargs['is_voice_note'] = True + return self.send_file(*args, **kwargs) def _send_album(self, entity, files, caption=None, progress_callback=None, reply_to=None): From 0b662f3b043907e872e49931201dc0fd923c63dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Tue, 27 Feb 2018 11:30:42 +0100 Subject: [PATCH 223/361] Support stopping propagation of events (#622) --- .../extra/basic/working-with-updates.rst | 27 +++++++++++++++++++ telethon/events/__init__.py | 23 ++++++++++++++++ telethon/telegram_client.py | 10 ++++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index a6c0a529..df971d7b 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -121,6 +121,33 @@ random number, while if you say ``'eval 4+4'``, you will reply with the solution. Try it! +Stopping propagation of Updates +******************************* + +There might be cases when an event handler is supposed to be used solitary and +it makes no sense to process any other handlers in the chain. For this case, +it is possible to raise a ``StopPropagation`` exception which will cause the +propagation of the update through your handlers to stop: + + .. code-block:: python + + from telethon.events import StopPropagation + + @client.on(events.NewMessage) + def _(event): + # ... some conditions + event.delete() + + # Other handlers won't have an event to work with + raise StopPropagation + + @client.on(events.NewMessage) + def _(event): + # Will never be reached, because it is the second handler + # in the chain. + pass + + Events module ************* diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 48b26004..c5c87fbd 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -873,3 +873,26 @@ class MessageChanged(_EventBuilder): self.edited = bool(edit_msg) self.deleted = bool(deleted_ids) self.deleted_ids = deleted_ids or [] + + +class StopPropagation(Exception): + """ + If this Exception is found to be raised in any of the handlers for a + given update, it will stop the execution of all other registered + event handlers in the chain. + Think of it like a ``StopIteration`` exception in a for loop. + + Example usage: + ``` + @client.on(events.NewMessage) + def delete(event): + event.delete() + # Other handlers won't have an event to work with + raise StopPropagation + + @client.on(events.NewMessage) + def _(event): + # Will never be reached, because it is the second handler in the chain. + pass + ``` + """ diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a8270aaf..00763000 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1885,7 +1885,15 @@ class TelegramClient(TelegramBareClient): event = builder.build(update) if event: event._client = self - callback(event) + try: + callback(event) + except events.StopPropagation: + __log__.debug( + "Event handler '{}' stopped chain of " + "propagation for event {}." + .format(callback.__name__, type(event).__name__) + ) + break def add_event_handler(self, callback, event=None): """ From d5832e4f3be8ae8afcabc14b3075fa19990cacf4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 27 Feb 2018 11:41:45 +0100 Subject: [PATCH 224/361] Fix time offset failing if system time was ahead of time While the offset was working, the last message ID was never reset, so it would always pick an higher message ID (safety check), which completely defeated the purpose of negative time offsets. Should close #496. --- telethon/session.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/telethon/session.py b/telethon/session.py index faa1516f..6b374c39 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -325,10 +325,10 @@ class Session: """Generates a new unique message ID based on the current time (in ms) since epoch""" # Refer to mtproto_plain_sender.py for the original method - now = time.time() + now = time.time() + self.time_offset nanoseconds = int((now - int(now)) * 1e+9) # "message identifiers are divisible by 4" - new_msg_id = ((int(now) + self.time_offset) << 32) | (nanoseconds << 2) + new_msg_id = (int(now) << 32) | (nanoseconds << 2) with self._msg_id_lock: if self._last_msg_id >= new_msg_id: @@ -343,6 +343,7 @@ class Session: now = int(time.time()) correct = correct_msg_id >> 32 self.time_offset = correct - now + self._last_msg_id = 0 # Entity processing From 229cd78df08a0778bf09511fd2014f2d198dfbce Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 27 Feb 2018 14:10:02 +0100 Subject: [PATCH 225/361] Fix markdown's URL regex not acceping newlines --- telethon/extensions/markdown.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index e1fd04b7..a5dde5c6 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -21,10 +21,7 @@ DEFAULT_DELIMITERS = { '```': MessageEntityPre } -# Regex used to match r'\[(.+?)\]\((.+?)\)' (for URLs. -DEFAULT_URL_RE = re.compile(r'\[(.+?)\]\((.+?)\)') - -# Reverse operation for DEFAULT_URL_RE. {0} for text, {1} for URL. +DEFAULT_URL_RE = re.compile(r'\[([^\]]+)\]\((.+?)\)') DEFAULT_URL_FORMAT = '[{0}]({1})' From 057c6a0b12f7ae36bc073c5383e9cc143afd7679 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 28 Feb 2018 13:24:44 +0100 Subject: [PATCH 226/361] Support getting more than 10k members on .get_participants() Discussed on #580, original PR made on #639. --- telethon/telegram_client.py | 70 ++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 00763000..ce3dbbc3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1015,48 +1015,70 @@ class TelegramClient(TelegramBareClient): raise TypeError('Invalid message type: {}'.format(type(message))) - def get_participants(self, entity, limit=None, search=''): + def get_participants(self, entity, limit=None, search='', + aggressive=False): """ - Gets the list of participants from the specified entity + Gets the list of participants from the specified entity. Args: entity (:obj:`entity`): The entity from which to retrieve the participants list. - limit (:obj: `int`): + limit (:obj:`int`): Limits amount of participants fetched. - search (:obj: `str`, optional): + search (:obj:`str`, optional): Look for participants with this string in name/username. + aggressive (:obj:`bool`, optional): + Aggressively looks for all participants in the chat in + order to get more than 10,000 members (a hard limit + imposed by Telegram). Note that this might make over + 20 times more requests and take over 5 minutes in some + cases, but often returns well over 10,000 members. + + This has no effect for groups or channels with less than + 10,000 members. + Returns: - A list of participants with an additional .total variable on the list - indicating the total amount of members in this group/channel. + A list of participants with an additional .total variable on the + list indicating the total amount of members in this group/channel. """ entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) if isinstance(entity, InputPeerChannel): - offset = 0 + total = self(GetFullChannelRequest( + entity + )).full_chat.participants_count + all_participants = {} - search = ChannelParticipantsSearch(search) - while True: - loop_limit = min(limit - offset, 200) - participants = self(GetParticipantsRequest( - entity, search, offset, loop_limit, hash=0 - )) - if not participants.users: - break - for user in participants.users: - if len(all_participants) < limit: - all_participants[user.id] = user - offset += len(participants.users) - if offset > limit: - break + if total > 10000 and aggressive: + searches = tuple(chr(x) for x in range(ord('a'), ord('z') + 1)) + else: + searches = ('',) + + for extra_search in searches: + req = GetParticipantsRequest( + channel=entity, + filter=ChannelParticipantsSearch(search + extra_search), + offset=0, + limit=0, + hash=0 + ) + while True: + req.limit = min(limit - req.offset, 200) + participants = self(req) + if not participants.users: + break + for user in participants.users: + if len(all_participants) < limit: + all_participants[user.id] = user + req.offset += len(participants.users) + if req.offset > limit: + break users = UserList(all_participants.values()) - users.total = self(GetFullChannelRequest( - entity)).full_chat.participants_count - + users.total = total elif isinstance(entity, InputPeerChat): users = self(GetFullChatRequest(entity.chat_id)).users if len(users) > limit: From 3afd7dca8435bf9402632331892695977eb89a1b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 28 Feb 2018 17:10:44 +0100 Subject: [PATCH 227/361] Invoke multiple getParticipant's at the same time (#580) --- telethon/telegram_client.py | 66 +++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ce3dbbc3..f2c1074b 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1033,9 +1033,9 @@ class TelegramClient(TelegramBareClient): aggressive (:obj:`bool`, optional): Aggressively looks for all participants in the chat in order to get more than 10,000 members (a hard limit - imposed by Telegram). Note that this might make over - 20 times more requests and take over 5 minutes in some - cases, but often returns well over 10,000 members. + imposed by Telegram). Note that this might take a long + time (over 5 minutes), but is able to return over 90,000 + participants on groups with 100,000 members. This has no effect for groups or channels with less than 10,000 members. @@ -1053,31 +1053,49 @@ class TelegramClient(TelegramBareClient): all_participants = {} if total > 10000 and aggressive: - searches = tuple(chr(x) for x in range(ord('a'), ord('z') + 1)) - else: - searches = ('',) - - for extra_search in searches: - req = GetParticipantsRequest( + requests = [GetParticipantsRequest( channel=entity, - filter=ChannelParticipantsSearch(search + extra_search), + filter=ChannelParticipantsSearch(search + chr(x)), offset=0, - limit=0, + limit=200, hash=0 - ) - while True: - req.limit = min(limit - req.offset, 200) - participants = self(req) - if not participants.users: - break - for user in participants.users: - if len(all_participants) < limit: - all_participants[user.id] = user - req.offset += len(participants.users) - if req.offset > limit: - break + ) for x in range(ord('a'), ord('z') + 1)] + else: + requests = [GetParticipantsRequest( + channel=entity, + filter=ChannelParticipantsSearch(search), + offset=0, + limit=200, + hash=0 + )] - users = UserList(all_participants.values()) + while requests: + # Only care about the limit for the first request + # (small amount of people, won't be aggressive). + # + # Most people won't care about getting exactly 12,345 + # members so it doesn't really matter not to be 100% + # precise with being out of the offset/limit here. + requests[0].limit = min(limit - requests[0].offset, 200) + if requests[0].offset > limit: + break + + results = self(*requests) + for i in reversed(range(len(requests))): + participants = results[i] + if not participants.users: + requests.pop(i) + else: + requests[i].offset += len(participants.users) + for user in participants.users: + if len(all_participants) < limit: + all_participants[user.id] = user + if limit < float('inf'): + values = all_participants.values() + else: + values = itertools.islice(all_participants.values(), limit) + + users = UserList(values) users.total = total elif isinstance(entity, InputPeerChat): users = self(GetFullChatRequest(entity.chat_id)).users From 3655df50dcd894dbddb9f3b19227087684c7a2d3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 28 Feb 2018 21:09:05 +0100 Subject: [PATCH 228/361] Postpone events resolution --- telethon/telegram_client.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f2c1074b..73cdbfe9 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -174,6 +174,7 @@ class TelegramClient(TelegramBareClient): ) self._event_builders = [] + self._events_pending_resolve = [] # Some fields to easy signing in. Let {phone: hash} be # a dictionary because the user may change their mind. @@ -288,6 +289,7 @@ class TelegramClient(TelegramBareClient): self.connect() if self.is_user_authorized(): + self._check_events_pending_resolve() return self if bot_token: @@ -344,6 +346,7 @@ class TelegramClient(TelegramBareClient): # We won't reach here if any step failed (exit by exception) print('Signed in successfully as', utils.get_display_name(me)) + self._check_events_pending_resolve() return self def sign_in(self, phone=None, code=None, @@ -377,6 +380,9 @@ class TelegramClient(TelegramBareClient): The signed in user, or the information about :meth:`.send_code_request()`. """ + if self.is_user_authorized(): + self._check_events_pending_resolve() + return self.get_me() if phone and not code and not password: return self.send_code_request(phone) @@ -435,6 +441,10 @@ class TelegramClient(TelegramBareClient): Returns: The new created user. """ + if self.is_user_authorized(): + self._check_events_pending_resolve() + return self.get_me() + result = self(SignUpRequest( phone_number=self._phone, phone_code_hash=self._phone_code_hash.get(self._phone, ''), @@ -1920,6 +1930,12 @@ class TelegramClient(TelegramBareClient): return decorator + def _check_events_pending_resolve(self): + if self._events_pending_resolve: + for event in self._events_pending_resolve: + event.resolve(self) + self._events_pending_resolve.clear() + def _on_handler(self, update): for builder, callback in self._event_builders: event = builder.build(update) @@ -1963,7 +1979,12 @@ class TelegramClient(TelegramBareClient): elif not event: event = events.Raw() - event.resolve(self) + if self.is_user_authorized(): + event.resolve(self) + self._check_events_pending_resolve() + else: + self._events_pending_resolve.append(event) + self._event_builders.append((event, callback)) def add_update_handler(self, handler): @@ -1987,6 +2008,10 @@ class TelegramClient(TelegramBareClient): # region Small utilities to make users' life easier + def _set_connected_and_authorized(self): + super()._set_connected_and_authorized() + self._check_events_pending_resolve() + def get_entity(self, entity): """ Turns the given entity into a valid Telegram user or chat. From 31846415490fe90d79e84b8c9e38b7b528207746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Thu, 1 Mar 2018 00:15:30 +0100 Subject: [PATCH 229/361] Allow access to events' pattern match (#654) --- telethon/events/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index c5c87fbd..5966a120 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -77,6 +77,8 @@ class _EventCommon(abc.ABC): self._input_chat = None self._chat = None + self.pattern_match = None + self.is_private = isinstance(chat_peer, types.PeerUser) self.is_group = ( isinstance(chat_peer, (types.PeerChat, types.PeerChannel)) @@ -251,8 +253,12 @@ class NewMessage(_EventBuilder): return if self.outgoing and not event.message.out: return - if self.pattern and not self.pattern(event.message.message or ''): - return + + if self.pattern: + match = self.pattern(event.message.message or '') + if not match: + return + event.pattern_match = match return self._filter_event(event) From 835ff51e2520ea44ec57dc57a11d46d88a1e1007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Thu, 1 Mar 2018 13:21:28 +0100 Subject: [PATCH 230/361] Multiple small changes/fixed typos to docs/error messages (#623) --- .../extra/advanced-usage/update-modes.rst | 12 +++---- readthedocs/extra/basic/entities.rst | 31 ++++++++++--------- readthedocs/extra/basic/installation.rst | 8 +++-- .../extra/basic/working-with-updates.rst | 10 ++++++ .../telegram-api-in-other-languages.rst | 17 +++++++--- .../extra/examples/chats-and-channels.rst | 8 +++++ telethon/telegram_bare_client.py | 4 ++- telethon/telegram_client.py | 4 +-- telethon/tl/custom/draft.py | 6 ++-- telethon/utils.py | 6 ++-- 10 files changed, 71 insertions(+), 35 deletions(-) diff --git a/readthedocs/extra/advanced-usage/update-modes.rst b/readthedocs/extra/advanced-usage/update-modes.rst index 83495ef7..942af9c9 100644 --- a/readthedocs/extra/advanced-usage/update-modes.rst +++ b/readthedocs/extra/advanced-usage/update-modes.rst @@ -37,7 +37,7 @@ an `Update`__ arrives: def callback(update): print('I received', update) - client.add_update_handler(callback) + client.add_event_handler(callback) # do more work here, or simply sleep! That's it! This is the old way to listen for raw updates, with no further @@ -56,7 +56,7 @@ let's reply to them with the same text reversed: client.send_message(PeerUser(update.user_id), update.message[::-1]) - client.add_update_handler(replier) + client.add_event_handler(replier) input('Press enter to stop this!') client.disconnect() @@ -96,9 +96,9 @@ additional workers: ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` -You **must** set it to ``0`` (or other number), as it defaults to ``None`` -and there is a different. ``None`` workers means updates won't be processed -*at all*, so you must set it to some value (``0`` or greater) if you want +You **must** set it to ``0`` (or higher), as it defaults to ``None`` and that +has a different meaning. ``None`` workers means updates won't be processed +*at all*, so you must set it to some integer value if you want ``client.updates.poll()`` to work. @@ -134,7 +134,7 @@ As a complete example: update_workers=1, spawn_read_thread=False) client.connect() - client.add_update_handler(callback) + client.add_event_handler(callback) client.idle() # ends with Ctrl+C diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index 472942a7..b68a74d7 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -43,14 +43,15 @@ you're able to just do this: my_channel = client.get_entity(PeerChannel(some_id)) -All methods in the :ref:`telegram-client` call ``.get_input_entity()`` to -further save you from the hassle of doing so manually, so doing things like -``client.send_message('lonami', 'hi!')`` is possible. +All methods in the :ref:`telegram-client` call ``.get_input_entity()`` prior +to sending the requst to save you from the hassle of doing so manually. +That way, convenience calls such as ``client.send_message('lonami', 'hi!')`` +become possible. -Every entity the library "sees" (in any response to any call) will by -default be cached in the ``.session`` file, to avoid performing -unnecessary API calls. If the entity cannot be found, some calls -like ``ResolveUsernameRequest`` or ``GetContactsRequest`` may be +Every entity the library encounters (in any response to any call) will by +default be cached in the ``.session`` file (an SQLite database), to avoid +performing unnecessary API calls. If the entity cannot be found, additonal +calls like ``ResolveUsernameRequest`` or ``GetContactsRequest`` may be made to obtain the required information. @@ -61,16 +62,18 @@ Entities vs. Input Entities Don't worry if you don't understand this section, just remember some of the details listed here are important. When you're calling a method, - don't call ``.get_entity()`` before, just use the username or phone, + don't call ``.get_entity()`` beforehand, just use the username or phone, or the entity retrieved by other means like ``.get_dialogs()``. -To save bandwidth, the API also makes use of their "input" versions. -The input version of an entity (e.g. ``InputPeerUser``, ``InputChat``, -etc.) only contains the minimum required information that's required -for Telegram to be able to identify who you're referring to: their ID -and hash. This ID/hash pair is unique per user, so if you use the pair -given by another user **or bot** it will **not** work. +On top of the normal types, the API also make use of what they call their +``Input*`` versions of objects. The input version of an entity (e.g. +``InputPeerUser``, ``InputChat``, etc.) only contains the minimum +information that's required from Telegram to be able to identify +who you're referring to: a ``Peer``'s **ID** and **hash**. + +This ID/hash pair is unique per user, so if you use the pair given by another +user **or bot** it will **not** work. To save *even more* bandwidth, the API also makes use of the ``Peer`` versions, which just have an ID. This serves to identify them, but diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index 0f812127..c00ea79c 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -65,9 +65,10 @@ To generate the `method documentation`__, ``cd docs`` and then Optional dependencies ********************* -If ``libssl`` is available on your system, it will be used wherever encryption -is needed, but otherwise it will fall back to pure Python implementation so it -will also work without it. +If the `cryptg`__ is installed, you might notice a speed-up in the download +and upload speed, since these are the most cryptographic-heavy part of the +library and said module is a C extension. Otherwise, the ``pyaes`` fallback +will be used. __ https://github.com/ricmoo/pyaes @@ -75,3 +76,4 @@ __ https://pypi.python.org/pypi/pyaes __ https://github.com/sybrenstuvel/python-rsa __ https://pypi.python.org/pypi/rsa/3.4.2 __ https://lonamiwebs.github.io/Telethon +__ https://github.com/Lonami/cryptg diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index df971d7b..652f6000 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -10,6 +10,16 @@ over what Telegram calls `updates`__, and are meant to ease simple and common usage when dealing with them, since there are many updates. Let's dive in! +.. note:: + + The library logs by default no output, and any exception that occurs + inside your handlers will be "hidden" from you to prevent the thread + from terminating (so it can still deliver events). You should enable + logging (``import logging; logging.basicConfig(level=logging.ERROR)``) + when working with events, at least the error level, to see if this is + happening so you can debug the error. + + .. contents:: diff --git a/readthedocs/extra/developing/telegram-api-in-other-languages.rst b/readthedocs/extra/developing/telegram-api-in-other-languages.rst index 44e45d51..7637282e 100644 --- a/readthedocs/extra/developing/telegram-api-in-other-languages.rst +++ b/readthedocs/extra/developing/telegram-api-in-other-languages.rst @@ -18,6 +18,14 @@ there by `@vysheng `__, `telegram-cli `__. Latest development has been moved to `BitBucket `__. +C++ +*** + +The newest (and official) library, written from scratch, is called +`tdlib `__ and is what the Telegram X +uses. You can find more information in the official documentation, +published `here `__. + JavaScript ********** @@ -52,13 +60,14 @@ Python A fairly new (as of the end of 2017) Telegram library written from the ground up in Python by `@delivrance `__ and his -`Pyrogram `__ library! No hard -feelings Dan and good luck dealing with some of your users ;) +`Pyrogram `__ library. +There isn't really a reason to pick it over Telethon and it'd be kinda +sad to see you go, but it would be nice to know what you miss from each +other library in either one so both can improve. Rust **** Yet another work-in-progress implementation, this time for Rust thanks to `@JuanPotato `__ under the fancy -name of `Vail `__. This one is very -early still, but progress is being made at a steady rate. +name of `Vail `__. diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 44ee6112..95fa1b1e 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -121,6 +121,13 @@ a fixed limit: offset += len(participants.users) +.. note:: + + It is **not** possible to get more than 10,000 members from a + group. It's a hard limit impossed by Telegram and there is + nothing you can do about it. Refer to `issue 573`__ for more. + + Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__, which may have more information you need (like the role of the participants, total count of members, etc.) @@ -130,6 +137,7 @@ __ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html __ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html __ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html __ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html +__ https://github.com/LonamiWebs/Telethon/issues/573 Recent Actions diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 23fd4ee4..8a15476e 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -455,7 +455,9 @@ class TelegramBareClient: with self._reconnect_lock: self._reconnect() - raise RuntimeError('Number of retries reached 0.') + raise RuntimeError('Number of retries reached 0 for {}.'.format( + [type(x).__name__ for x in requests] + )) # Let people use client.invoke(SomeRequest()) instead client(...) invoke = __call__ diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 73cdbfe9..6cd1d6eb 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -586,7 +586,7 @@ class TelegramClient(TelegramBareClient): Returns: A list of custom ``Draft`` objects that are easy to work with: - You can call :meth:`draft.set_message('text')` to change the message, + You can call ``draft.set_message('text')`` to change the message, or delete it through :meth:`draft.delete()`. """ response = self(GetAllDraftsRequest()) @@ -2193,7 +2193,7 @@ class TelegramClient(TelegramBareClient): return utils.get_input_peer(entity) raise TypeError( - 'Could not find the input entity corresponding to "{}".' + 'Could not find the input entity corresponding to "{}". ' 'Make sure you have encountered this peer before.'.format(peer) ) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index ae08403a..9b800d4c 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -42,7 +42,7 @@ class Draft: """ 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 + example the ``reply_to_msg_id`` should be done by providing the current values of this object, like so: draft.set_message( @@ -56,7 +56,7 @@ class 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 + :return bool: ``True`` on success """ result = self._client(SaveDraftRequest( peer=self._peer, @@ -77,6 +77,6 @@ class Draft: def delete(self): """ Deletes this draft - :return bool: `True` on success + :return bool: ``True`` on success """ return self.set_message(text='') diff --git a/telethon/utils.py b/telethon/utils.py index 8f38563a..ed93bdfa 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -35,8 +35,10 @@ VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$') def get_display_name(entity): - """Gets the input peer for the given "entity" (user, chat or channel) - Returns None if it was not found""" + """ + Gets the display name for the given entity, if it's an ``User``, + ``Chat`` or ``Channel``. Returns an empty string otherwise. + """ if isinstance(entity, User): if entity.last_name and entity.first_name: return '{} {}'.format(entity.first_name, entity.last_name) From 3d68c879dd08fdff5d8df0aab90fc750c1b7668b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Mar 2018 13:25:38 +0100 Subject: [PATCH 231/361] Avoid using undefined variable in the README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index febc43cd..a2e0d3de 100755 --- a/README.rst +++ b/README.rst @@ -46,12 +46,12 @@ Doing stuff .. code:: python - print(me.stringify()) + print(client.get_me().stringify()) client.send_message('username', 'Hello! Talking to you from Telethon') client.send_file('username', '/home/myself/Pictures/holidays.jpg') - client.download_profile_photo(me) + client.download_profile_photo('me') messages = client.get_message_history('username') client.download_media(messages[0]) From 771c573db1304292492e9a65288cd4802ace40dc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Mar 2018 13:31:39 +0100 Subject: [PATCH 232/361] Better attempt at joining update worker threads --- telethon/update_state.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index 6fa0b12a..1ac2e00d 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -62,23 +62,23 @@ class UpdateState: """ self.stop_workers() self._workers = n - if n is None: - while self._updates: - self._updates.get() - else: + if n is not None: self.setup_workers() workers = property(fget=get_workers, fset=set_workers) def stop_workers(self): - """Raises "StopIterationException" on the worker threads to stop them, - and also clears all of them off the list + """ + Raises "StopIterationException" on the worker threads to stop + them, and also clears all the workers/updates from the lists. """ if self._workers: with self._updates_lock: # Insert at the beginning so the very next poll causes an error # on all the worker threads # TODO Should this reset the pts and such? + while self._updates: + self._updates.get() for _ in range(self._workers): self._updates.put(StopIteration()) From 3a3f221bd15fe312aacfd981ac07fd7714ea09e8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Mar 2018 20:03:51 +0100 Subject: [PATCH 233/361] Look in all dialogs when getting entities by peer ID --- telethon/telegram_client.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 6cd1d6eb..9690fcb4 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -572,7 +572,7 @@ class TelegramClient(TelegramBareClient): offset_date = r.messages[-1].date offset_peer = entities[utils.get_peer_id(r.dialogs[-1].peer)] - offset_id = r.messages[-1].id & 4294967296 # Telegram/danog magic + offset_id = r.messages[-1].id dialogs = UserList( itertools.islice(dialogs.values(), min(limit, len(dialogs))) @@ -2175,22 +2175,32 @@ class TelegramClient(TelegramBareClient): 'Cannot turn "{}" into an input entity.'.format(peer) ) - # 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. - dialogs = self(GetDialogsRequest( + # Not found, look in the dialogs with the hope to find it. + target_id = utils.get_peer_id(peer) + req = GetDialogsRequest( offset_date=None, offset_id=0, offset_peer=InputPeerEmpty(), - limit=0, - exclude_pinned=True - )) + limit=100 + ) + while True: + result = self(req) + entities = {} + for x in itertools.chain(result.users, result.chats): + x_id = utils.get_peer_id(x) + if x_id == target_id: + return utils.get_input_peer(x) + else: + entities[x_id] = x + if len(result.dialogs) < req.limit: + break - target = utils.get_peer_id(peer) - for entity in itertools.chain(dialogs.users, dialogs.chats): - if utils.get_peer_id(entity) == target: - return utils.get_input_peer(entity) + req.offset_id = result.messages[-1].id + req.offset_date = result.messages[-1].date + req.offset_peer = entities[utils.get_peer_id( + result.dialogs[-1].peer + )] + time.sleep(1) raise TypeError( 'Could not find the input entity corresponding to "{}". ' From f09ab6c6b6cc1ff84cc0d285a6c88c4f32705cc2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Mar 2018 20:13:21 +0100 Subject: [PATCH 234/361] Fix-up 771c573 to properly stop background update workers The "special" StopIteration object didn't actually make any sense. Instead looping forever, workers now loop while there are workers, so that they stop looping once the count is cleared. Dummy values are still inserted so that they don't need to timeout on the queue before exiting (these values are None) so in essence, this keeps the best of both of worlds. --- telethon/update_state.py | 43 +++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index 1ac2e00d..98345cdf 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -39,19 +39,15 @@ class UpdateState: return not self._updates.empty() def poll(self, timeout=None): - """Polls an update or blocks until an update object is available. - If 'timeout is not None', it should be a floating point value, - and the method will 'return None' if waiting times out. + """ + Polls an update or blocks until an update object is available. + If 'timeout is not None', it should be a floating point value, + and the method will 'return None' if waiting times out. """ try: - update = self._updates.get(timeout=timeout) + return self._updates.get(timeout=timeout) except Empty: - return - - if isinstance(update, Exception): - raise update # Some error was set through (surely StopIteration) - - return update + return None def get_workers(self): return self._workers @@ -60,27 +56,24 @@ class UpdateState: """Changes the number of workers running. If 'n is None', clears all pending updates from memory. """ - self.stop_workers() - self._workers = n - if n is not None: + if n is None: + self.stop_workers() + else: + self._workers = n self.setup_workers() workers = property(fget=get_workers, fset=set_workers) def stop_workers(self): """ - Raises "StopIterationException" on the worker threads to stop - them, and also clears all the workers/updates from the lists. + Waits for all the worker threads to stop. """ - if self._workers: - with self._updates_lock: - # Insert at the beginning so the very next poll causes an error - # on all the worker threads - # TODO Should this reset the pts and such? - while self._updates: - self._updates.get() - for _ in range(self._workers): - self._updates.put(StopIteration()) + # Put dummy ``None`` objects so that they don't need to timeout. + n = self._workers + self._workers = None + with self._updates_lock: + for _ in range(n): + self._updates.put(None) for t in self._worker_threads: t.join() @@ -103,7 +96,7 @@ class UpdateState: thread.start() def _worker_loop(self, wid): - while True: + while self._workers is not None: try: update = self.poll(timeout=UpdateState.WORKER_POLL_TIMEOUT) if update and self.handler: From c5e6f7e265227702ad39f3c9cf632524ff1274bf Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Mar 2018 23:34:32 +0200 Subject: [PATCH 235/361] Split Session into three parts and make a module for sessions --- telethon/network/mtproto_sender.py | 4 +- telethon/sessions/__init__.py | 3 + telethon/sessions/abstract.py | 136 +++++++++ telethon/sessions/memory.py | 297 ++++++++++++++++++++ telethon/{session.py => sessions/sqlite.py} | 234 ++++----------- telethon/telegram_bare_client.py | 10 +- 6 files changed, 491 insertions(+), 193 deletions(-) create mode 100644 telethon/sessions/__init__.py create mode 100644 telethon/sessions/abstract.py create mode 100644 telethon/sessions/memory.py rename telethon/{session.py => sessions/sqlite.py} (59%) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 43b5e803..cbcdc76d 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -402,13 +402,13 @@ class MtProtoSender: elif bad_msg.error_code == 32: # msg_seqno too low, so just pump it up by some "large" amount # TODO A better fix would be to start with a new fresh session ID - self.session._sequence += 64 + self.session.sequence += 64 __log__.info('Attempting to set the right higher sequence') self._resend_request(bad_msg.bad_msg_id) return True elif bad_msg.error_code == 33: # msg_seqno too high never seems to happen but just in case - self.session._sequence -= 16 + self.session.sequence -= 16 __log__.info('Attempting to set the right lower sequence') self._resend_request(bad_msg.bad_msg_id) return True diff --git a/telethon/sessions/__init__.py b/telethon/sessions/__init__.py new file mode 100644 index 00000000..af3423f3 --- /dev/null +++ b/telethon/sessions/__init__.py @@ -0,0 +1,3 @@ +from .abstract import Session +from .memory import MemorySession +from .sqlite import SQLiteSession diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py new file mode 100644 index 00000000..c7392ffc --- /dev/null +++ b/telethon/sessions/abstract.py @@ -0,0 +1,136 @@ +from abc import ABC, abstractmethod + + +class Session(ABC): + @abstractmethod + def clone(self): + raise NotImplementedError + + @abstractmethod + def set_dc(self, dc_id, server_address, port): + raise NotImplementedError + + @property + @abstractmethod + def server_address(self): + raise NotImplementedError + + @property + @abstractmethod + def port(self): + raise NotImplementedError + + @property + @abstractmethod + def auth_key(self): + raise NotImplementedError + + @auth_key.setter + @abstractmethod + def auth_key(self, value): + raise NotImplementedError + + @property + @abstractmethod + def time_offset(self): + raise NotImplementedError + + @time_offset.setter + @abstractmethod + def time_offset(self, value): + raise NotImplementedError + + @property + @abstractmethod + def salt(self): + raise NotImplementedError + + @salt.setter + @abstractmethod + def salt(self, value): + raise NotImplementedError + + @property + @abstractmethod + def device_model(self): + raise NotImplementedError + + @property + @abstractmethod + def system_version(self): + raise NotImplementedError + + @property + @abstractmethod + def app_version(self): + raise NotImplementedError + + @property + @abstractmethod + def lang_code(self): + raise NotImplementedError + + @property + @abstractmethod + def system_lang_code(self): + raise NotImplementedError + + @property + @abstractmethod + def report_errors(self): + raise NotImplementedError + + @property + @abstractmethod + def sequence(self): + raise NotImplementedError + + @property + @abstractmethod + def flood_sleep_threshold(self): + raise NotImplementedError + + @abstractmethod + def close(self): + raise NotImplementedError + + @abstractmethod + def save(self): + raise NotImplementedError + + @abstractmethod + def delete(self): + raise NotImplementedError + + @classmethod + @abstractmethod + def list_sessions(cls): + raise NotImplementedError + + @abstractmethod + def get_new_msg_id(self): + raise NotImplementedError + + @abstractmethod + def update_time_offset(self, correct_msg_id): + raise NotImplementedError + + @abstractmethod + def generate_sequence(self, content_related): + raise NotImplementedError + + @abstractmethod + def process_entities(self, tlo): + raise NotImplementedError + + @abstractmethod + def get_input_entity(self, key): + raise NotImplementedError + + @abstractmethod + def cache_file(self, md5_digest, file_size, instance): + raise NotImplementedError + + @abstractmethod + def get_file(self, md5_digest, file_size, cls): + raise NotImplementedError diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py new file mode 100644 index 00000000..66558829 --- /dev/null +++ b/telethon/sessions/memory.py @@ -0,0 +1,297 @@ +from enum import Enum +import time +import platform + +from .. import utils +from .abstract import Session +from ..tl import TLObject + +from ..tl.types import ( + PeerUser, PeerChat, PeerChannel, + InputPeerUser, InputPeerChat, InputPeerChannel, + InputPhoto, InputDocument +) + + +class _SentFileType(Enum): + DOCUMENT = 0 + PHOTO = 1 + + @staticmethod + def from_type(cls): + if cls == InputDocument: + return _SentFileType.DOCUMENT + elif cls == InputPhoto: + return _SentFileType.PHOTO + else: + raise ValueError('The cls must be either InputDocument/InputPhoto') + + +class MemorySession(Session): + def __init__(self): + self._dc_id = None + self._server_address = None + self._port = None + self._salt = None + self._auth_key = None + self._sequence = 0 + self._last_msg_id = 0 + self._time_offset = 0 + self._flood_sleep_threshold = 60 + + system = platform.uname() + self._device_model = system.system or 'Unknown' + self._system_version = system.release or '1.0' + self._app_version = '1.0' + self._lang_code = 'en' + self._system_lang_code = self.lang_code + self._report_errors = True + self._flood_sleep_threshold = 60 + + self._files = {} + self._entities = set() + + def clone(self): + cloned = MemorySession() + cloned._device_model = self.device_model + cloned._system_version = self.system_version + cloned._app_version = self.app_version + cloned._lang_code = self.lang_code + cloned._system_lang_code = self.system_lang_code + cloned._report_errors = self.report_errors + cloned._flood_sleep_threshold = self.flood_sleep_threshold + + def set_dc(self, dc_id, server_address, port): + self._dc_id = dc_id + self._server_address = server_address + self._port = port + + @property + def server_address(self): + return self._server_address + + @property + def port(self): + return self._port + + @property + def auth_key(self): + return self._auth_key + + @auth_key.setter + def auth_key(self, value): + self._auth_key = value + + @property + def time_offset(self): + return self._time_offset + + @time_offset.setter + def time_offset(self, value): + self._time_offset = value + + @property + def salt(self): + return self._salt + + @salt.setter + def salt(self, value): + self._salt = value + + @property + def device_model(self): + return self._device_model + + @property + def system_version(self): + return self._system_version + + @property + def app_version(self): + return self._app_version + + @property + def lang_code(self): + return self._lang_code + + @property + def system_lang_code(self): + return self._system_lang_code + + @property + def report_errors(self): + return self._report_errors + + @property + def sequence(self): + return self._sequence + + @property + def flood_sleep_threshold(self): + return self._flood_sleep_threshold + + def close(self): + pass + + def save(self): + pass + + def delete(self): + pass + + @classmethod + def list_sessions(cls): + raise NotImplementedError + + def get_new_msg_id(self): + """Generates a new unique message ID based on the current + time (in ms) since epoch""" + now = time.time() + self._time_offset + nanoseconds = int((now - int(now)) * 1e+9) + new_msg_id = (int(now) << 32) | (nanoseconds << 2) + + if self._last_msg_id >= new_msg_id: + new_msg_id = self._last_msg_id + 4 + + self._last_msg_id = new_msg_id + + return new_msg_id + + def update_time_offset(self, correct_msg_id): + now = int(time.time()) + correct = correct_msg_id >> 32 + self._time_offset = correct - now + self._last_msg_id = 0 + + def generate_sequence(self, content_related): + if content_related: + result = self._sequence * 2 + 1 + self._sequence += 1 + return result + else: + return self._sequence * 2 + + @staticmethod + def _entities_to_rows(tlo): + if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): + # This may be a list of users already for instance + entities = tlo + else: + entities = [] + if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats): + entities.extend(tlo.chats) + if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): + entities.extend(tlo.users) + if not entities: + return + + rows = [] # Rows to add (id, hash, username, phone, name) + for e in entities: + if not isinstance(e, TLObject): + continue + try: + p = utils.get_input_peer(e, allow_self=False) + marked_id = utils.get_peer_id(p) + except ValueError: + continue + + if isinstance(p, (InputPeerUser, InputPeerChannel)): + if not p.access_hash: + # Some users and channels seem to be returned without + # an 'access_hash', meaning Telegram doesn't want you + # to access them. This is the reason behind ensuring + # that the 'access_hash' is non-zero. See issue #354. + # Note that this checks for zero or None, see #392. + continue + else: + p_hash = p.access_hash + elif isinstance(p, InputPeerChat): + p_hash = 0 + else: + continue + + username = getattr(e, 'username', None) or None + if username is not None: + username = username.lower() + phone = getattr(e, 'phone', None) + name = utils.get_display_name(e) or None + rows.append((marked_id, p_hash, username, phone, name)) + return rows + + def process_entities(self, tlo): + self._entities += set(self._entities_to_rows(tlo)) + + def get_entity_rows_by_phone(self, phone): + rows = [(id, hash) for id, hash, _, found_phone, _ + in self._entities if found_phone == phone] + return rows[0] if rows else None + + def get_entity_rows_by_username(self, username): + rows = [(id, hash) for id, hash, found_username, _, _ + in self._entities if found_username == username] + return rows[0] if rows else None + + def get_entity_rows_by_name(self, name): + rows = [(id, hash) for id, hash, _, _, found_name + in self._entities if found_name == name] + return rows[0] if rows else None + + def get_entity_rows_by_id(self, id): + rows = [(id, hash) for found_id, hash, _, _, _ + in self._entities if found_id == id] + return rows[0] if rows else None + + def get_input_entity(self, key): + try: + if key.SUBCLASS_OF_ID in (0xc91c90b6, 0xe669bf46, 0x40f202fd): + # hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel')) + # We already have an Input version, so nothing else required + return key + # Try to early return if this key can be casted as input peer + return utils.get_input_peer(key) + except (AttributeError, TypeError): + # Not a TLObject or can't be cast into InputPeer + if isinstance(key, TLObject): + key = utils.get_peer_id(key) + + result = None + if isinstance(key, str): + phone = utils.parse_phone(key) + if phone: + result = self.get_entity_rows_by_phone(phone) + else: + username, _ = utils.parse_username(key) + if username: + result = self.get_entity_rows_by_username(username) + + if isinstance(key, int): + result = self.get_entity_rows_by_id(key) + + if not result and isinstance(key, str): + result = self.get_entity_rows_by_name(key) + + if result: + i, h = result # unpack resulting tuple + i, k = utils.resolve_id(i) # removes the mark and returns kind + if k == PeerUser: + return InputPeerUser(i, h) + elif k == PeerChat: + return InputPeerChat(i) + elif k == PeerChannel: + return InputPeerChannel(i, h) + else: + raise ValueError('Could not find input entity with key ', key) + + def cache_file(self, md5_digest, file_size, instance): + if not isinstance(instance, (InputDocument, InputPhoto)): + raise TypeError('Cannot cache %s instance' % type(instance)) + key = (md5_digest, file_size, _SentFileType.from_type(instance)) + value = (instance.id, instance.access_hash) + self._files[key] = value + + def get_file(self, md5_digest, file_size, cls): + key = (md5_digest, file_size, _SentFileType.from_type(cls)) + try: + return self._files[key] + except KeyError: + return None diff --git a/telethon/session.py b/telethon/sessions/sqlite.py similarity index 59% rename from telethon/session.py rename to telethon/sessions/sqlite.py index 6b374c39..66a0c887 100644 --- a/telethon/session.py +++ b/telethon/sessions/sqlite.py @@ -5,14 +5,15 @@ import sqlite3 import struct import time from base64 import b64decode -from enum import Enum from os.path import isfile as file_exists from threading import Lock, RLock -from . import utils -from .crypto import AuthKey -from .tl import TLObject -from .tl.types import ( +from .. import utils +from .abstract import Session +from .memory import MemorySession, _SentFileType +from ..crypto import AuthKey +from ..tl import TLObject +from ..tl.types import ( PeerUser, PeerChat, PeerChannel, InputPeerUser, InputPeerChat, InputPeerChannel, InputPhoto, InputDocument @@ -22,21 +23,7 @@ EXTENSION = '.session' CURRENT_VERSION = 3 # database version -class _SentFileType(Enum): - DOCUMENT = 0 - PHOTO = 1 - - @staticmethod - def from_type(cls): - if cls == InputDocument: - return _SentFileType.DOCUMENT - elif cls == InputPhoto: - return _SentFileType.PHOTO - else: - raise ValueError('The cls must be either InputDocument/InputPhoto') - - -class Session: +class SQLiteSession(MemorySession): """This session contains the required information to login into your Telegram account. NEVER give the saved JSON file to anyone, since they would gain instant access to all your messages and contacts. @@ -44,7 +31,9 @@ class Session: If you think the session has been compromised, close all the sessions through an official Telegram client to revoke the authorization. """ + def __init__(self, session_id): + super().__init__() """session_user_id should either be a string or another Session. Note that if another session is given, only parameters like those required to init a connection will be copied. @@ -54,15 +43,15 @@ class Session: # For connection purposes if isinstance(session_id, Session): - self.device_model = session_id.device_model - self.system_version = session_id.system_version - self.app_version = session_id.app_version - self.lang_code = session_id.lang_code - self.system_lang_code = session_id.system_lang_code - self.lang_pack = session_id.lang_pack - self.report_errors = session_id.report_errors - self.save_entities = session_id.save_entities - self.flood_sleep_threshold = session_id.flood_sleep_threshold + self._device_model = session_id.device_model + self._system_version = session_id.system_version + self._app_version = session_id.app_version + self._lang_code = session_id.lang_code + self._system_lang_code = session_id.system_lang_code + self._report_errors = session_id.report_errors + self._flood_sleep_threshold = session_id.flood_sleep_threshold + if isinstance(session_id, SQLiteSession): + self.save_entities = session_id.save_entities else: # str / None if session_id: self.filename = session_id @@ -70,15 +59,14 @@ class Session: self.filename += EXTENSION system = platform.uname() - self.device_model = system.system or 'Unknown' - self.system_version = system.release or '1.0' - self.app_version = '1.0' # '0' will provoke error - self.lang_code = 'en' - self.system_lang_code = self.lang_code - self.lang_pack = '' - self.report_errors = True + self._device_model = system.system or 'Unknown' + self._system_version = system.release or '1.0' + self._app_version = '1.0' # '0' will provoke error + self._lang_code = 'en' + self._system_lang_code = self.lang_code + self._report_errors = True self.save_entities = True - self.flood_sleep_threshold = 60 + self._flood_sleep_threshold = 60 self.id = struct.unpack('q', os.urandom(8))[0] self._sequence = 0 @@ -163,6 +151,9 @@ class Session: c.close() self.save() + def clone(self): + return SQLiteSession(self) + def _check_migrate_json(self): if file_exists(self.filename): try: @@ -233,19 +224,7 @@ class Session: self._auth_key = None c.close() - @property - def server_address(self): - return self._server_address - - @property - def port(self): - return self._port - - @property - def auth_key(self): - return self._auth_key - - @auth_key.setter + @Session.auth_key.setter def auth_key(self, value): self._auth_key = value self._update_session_table() @@ -298,53 +277,14 @@ class Session: except OSError: return False - @staticmethod - def list_sessions(): + @classmethod + def list_sessions(cls): """Lists all the sessions of the users who have ever connected using this client and never logged out """ return [os.path.splitext(os.path.basename(f))[0] for f in os.listdir('.') if f.endswith(EXTENSION)] - def generate_sequence(self, content_related): - """Thread safe method to generates the next sequence number, - based on whether it was confirmed yet or not. - - Note that if confirmed=True, the sequence number - will be increased by one too - """ - with self._seq_no_lock: - if content_related: - result = self._sequence * 2 + 1 - self._sequence += 1 - return result - else: - return self._sequence * 2 - - def get_new_msg_id(self): - """Generates a new unique message ID based on the current - time (in ms) since epoch""" - # Refer to mtproto_plain_sender.py for the original method - now = time.time() + self.time_offset - nanoseconds = int((now - int(now)) * 1e+9) - # "message identifiers are divisible by 4" - new_msg_id = (int(now) << 32) | (nanoseconds << 2) - - with self._msg_id_lock: - if self._last_msg_id >= new_msg_id: - new_msg_id = self._last_msg_id + 4 - - self._last_msg_id = new_msg_id - - return new_msg_id - - def update_time_offset(self, correct_msg_id): - """Updates the time offset based on a known correct message ID""" - now = int(time.time()) - correct = correct_msg_id >> 32 - self.time_offset = correct - now - self._last_msg_id = 0 - # Entity processing def process_entities(self, tlo): @@ -356,49 +296,7 @@ class Session: if not self.save_entities: return - if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): - # This may be a list of users already for instance - entities = tlo - else: - entities = [] - if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats): - entities.extend(tlo.chats) - if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): - entities.extend(tlo.users) - if not entities: - return - - rows = [] # Rows to add (id, hash, username, phone, name) - for e in entities: - if not isinstance(e, TLObject): - continue - try: - p = utils.get_input_peer(e, allow_self=False) - marked_id = utils.get_peer_id(p) - except ValueError: - continue - - if isinstance(p, (InputPeerUser, InputPeerChannel)): - if not p.access_hash: - # Some users and channels seem to be returned without - # an 'access_hash', meaning Telegram doesn't want you - # to access them. This is the reason behind ensuring - # that the 'access_hash' is non-zero. See issue #354. - # Note that this checks for zero or None, see #392. - continue - else: - p_hash = p.access_hash - elif isinstance(p, InputPeerChat): - p_hash = 0 - else: - continue - - username = getattr(e, 'username', None) or None - if username is not None: - username = username.lower() - phone = getattr(e, 'phone', None) - name = utils.get_display_name(e) or None - rows.append((marked_id, p_hash, username, phone, name)) + rows = self._entities_to_rows(tlo) if not rows: return @@ -408,62 +306,26 @@ class Session: ) self.save() - def get_input_entity(self, key): - """Parses the given string, integer or TLObject key into a - marked entity ID, which is then used to fetch the hash - from the database. - - If a callable key is given, every row will be fetched, - and passed as a tuple to a function, that should return - a true-like value when the desired row is found. - - Raises ValueError if it cannot be found. - """ - try: - if key.SUBCLASS_OF_ID in (0xc91c90b6, 0xe669bf46, 0x40f202fd): - # hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel')) - # We already have an Input version, so nothing else required - return key - # Try to early return if this key can be casted as input peer - return utils.get_input_peer(key) - except (AttributeError, TypeError): - # Not a TLObject or can't be cast into InputPeer - if isinstance(key, TLObject): - key = utils.get_peer_id(key) - + def _fetchone_entity(self, query, args): c = self._cursor() - if isinstance(key, str): - phone = utils.parse_phone(key) - if phone: - c.execute('select id, hash from entities where phone=?', - (phone,)) - else: - username, _ = utils.parse_username(key) - if username: - c.execute('select id, hash from entities where username=?', + c.execute(query, args) + return c.fetchone() + + def get_entity_rows_by_phone(self, phone): + return self._fetchone_entity( + 'select id, hash from entities where phone=?', (phone,)) + + def get_entity_rows_by_username(self, username): + self._fetchone_entity('select id, hash from entities where username=?', (username,)) - if isinstance(key, int): - c.execute('select id, hash from entities where id=?', (key,)) + def get_entity_rows_by_name(self, name): + self._fetchone_entity('select id, hash from entities where name=?', + (name,)) - result = c.fetchone() - if not result and isinstance(key, str): - # Try exact match by name if phone/username failed - c.execute('select id, hash from entities where name=?', (key,)) - result = c.fetchone() - - c.close() - if result: - i, h = result # unpack resulting tuple - i, k = utils.resolve_id(i) # removes the mark and returns kind - if k == PeerUser: - return InputPeerUser(i, h) - elif k == PeerChat: - return InputPeerChat(i) - elif k == PeerChannel: - return InputPeerChannel(i, h) - else: - raise ValueError('Could not find input entity with key ', key) + def get_entity_rows_by_id(self, id): + self._fetchone_entity('select id, hash from entities where id=?', + (id,)) # File processing diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 8a15476e..3a5b2bd0 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -14,7 +14,7 @@ from .errors import ( PhoneMigrateError, NetworkMigrateError, UserMigrateError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode -from .session import Session +from .sessions import Session, SQLiteSession from .tl import TLObject from .tl.all_tlobjects import LAYER from .tl.functions import ( @@ -81,10 +81,10 @@ class TelegramBareClient: "Refer to telethon.rtfd.io for more information.") self._use_ipv6 = use_ipv6 - + # Determine what session object we have if isinstance(session, str) or session is None: - session = Session(session) + session = SQLiteSession(session) elif not isinstance(session, Session): raise TypeError( 'The given session must be a str or a Session instance.' @@ -361,7 +361,7 @@ class TelegramBareClient: # # Construct this session with the connection parameters # (system version, device model...) from the current one. - session = Session(self.session) + session = self.session.clone() session.set_dc(dc.id, dc.ip_address, dc.port) self._exported_sessions[dc_id] = session @@ -387,7 +387,7 @@ class TelegramBareClient: session = self._exported_sessions.get(cdn_redirect.dc_id) if not session: dc = self._get_dc(cdn_redirect.dc_id, cdn=True) - session = Session(self.session) + session = self.session.clone() session.set_dc(dc.id, dc.ip_address, dc.port) self._exported_sessions[cdn_redirect.dc_id] = session From 4c64d53e7178b3555b9c9346b93c7b1f4ab3366f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 11:10:11 +0200 Subject: [PATCH 236/361] Move non-persistent stuff to base Session class --- telethon/sessions/abstract.py | 144 ++++++++++++++++++++-------------- telethon/sessions/memory.py | 62 +-------------- 2 files changed, 87 insertions(+), 119 deletions(-) diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index c7392ffc..ff0fd16d 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -1,10 +1,32 @@ from abc import ABC, abstractmethod +import time +import platform class Session(ABC): - @abstractmethod + def __init__(self): + self._sequence = 0 + self._last_msg_id = 0 + self._time_offset = 0 + + system = platform.uname() + self._device_model = system.system or 'Unknown' + self._system_version = system.release or '1.0' + self._app_version = '1.0' + self._lang_code = 'en' + self._system_lang_code = self.lang_code + self._report_errors = True + self._flood_sleep_threshold = 60 + def clone(self): - raise NotImplementedError + cloned = self.__class__() + cloned._device_model = self.device_model + cloned._system_version = self.system_version + cloned._app_version = self.app_version + cloned._lang_code = self.lang_code + cloned._system_lang_code = self.system_lang_code + cloned._report_errors = self.report_errors + cloned._flood_sleep_threshold = self.flood_sleep_threshold @abstractmethod def set_dc(self, dc_id, server_address, port): @@ -31,14 +53,12 @@ class Session(ABC): raise NotImplementedError @property - @abstractmethod def time_offset(self): - raise NotImplementedError + return self._time_offset @time_offset.setter - @abstractmethod def time_offset(self, value): - raise NotImplementedError + self._time_offset = value @property @abstractmethod @@ -50,46 +70,6 @@ class Session(ABC): def salt(self, value): raise NotImplementedError - @property - @abstractmethod - def device_model(self): - raise NotImplementedError - - @property - @abstractmethod - def system_version(self): - raise NotImplementedError - - @property - @abstractmethod - def app_version(self): - raise NotImplementedError - - @property - @abstractmethod - def lang_code(self): - raise NotImplementedError - - @property - @abstractmethod - def system_lang_code(self): - raise NotImplementedError - - @property - @abstractmethod - def report_errors(self): - raise NotImplementedError - - @property - @abstractmethod - def sequence(self): - raise NotImplementedError - - @property - @abstractmethod - def flood_sleep_threshold(self): - raise NotImplementedError - @abstractmethod def close(self): raise NotImplementedError @@ -107,18 +87,6 @@ class Session(ABC): def list_sessions(cls): raise NotImplementedError - @abstractmethod - def get_new_msg_id(self): - raise NotImplementedError - - @abstractmethod - def update_time_offset(self, correct_msg_id): - raise NotImplementedError - - @abstractmethod - def generate_sequence(self, content_related): - raise NotImplementedError - @abstractmethod def process_entities(self, tlo): raise NotImplementedError @@ -134,3 +102,63 @@ class Session(ABC): @abstractmethod def get_file(self, md5_digest, file_size, cls): raise NotImplementedError + + @property + def device_model(self): + return self._device_model + + @property + def system_version(self): + return self._system_version + + @property + def app_version(self): + return self._app_version + + @property + def lang_code(self): + return self._lang_code + + @property + def system_lang_code(self): + return self._system_lang_code + + @property + def report_errors(self): + return self._report_errors + + @property + def flood_sleep_threshold(self): + return self._flood_sleep_threshold + + @property + def sequence(self): + return self._sequence + + def get_new_msg_id(self): + """Generates a new unique message ID based on the current + time (in ms) since epoch""" + now = time.time() + self._time_offset + nanoseconds = int((now - int(now)) * 1e+9) + new_msg_id = (int(now) << 32) | (nanoseconds << 2) + + if self._last_msg_id >= new_msg_id: + new_msg_id = self._last_msg_id + 4 + + self._last_msg_id = new_msg_id + + return new_msg_id + + def update_time_offset(self, correct_msg_id): + now = int(time.time()) + correct = correct_msg_id >> 32 + self._time_offset = correct - now + self._last_msg_id = 0 + + def generate_sequence(self, content_related): + if content_related: + result = self._sequence * 2 + 1 + self._sequence += 1 + return result + else: + return self._sequence * 2 diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 66558829..09aa1fa0 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -1,6 +1,4 @@ from enum import Enum -import time -import platform from .. import utils from .abstract import Session @@ -29,38 +27,16 @@ class _SentFileType(Enum): class MemorySession(Session): def __init__(self): + super().__init__() self._dc_id = None self._server_address = None self._port = None self._salt = None self._auth_key = None - self._sequence = 0 - self._last_msg_id = 0 - self._time_offset = 0 - self._flood_sleep_threshold = 60 - - system = platform.uname() - self._device_model = system.system or 'Unknown' - self._system_version = system.release or '1.0' - self._app_version = '1.0' - self._lang_code = 'en' - self._system_lang_code = self.lang_code - self._report_errors = True - self._flood_sleep_threshold = 60 self._files = {} self._entities = set() - def clone(self): - cloned = MemorySession() - cloned._device_model = self.device_model - cloned._system_version = self.system_version - cloned._app_version = self.app_version - cloned._lang_code = self.lang_code - cloned._system_lang_code = self.system_lang_code - cloned._report_errors = self.report_errors - cloned._flood_sleep_threshold = self.flood_sleep_threshold - def set_dc(self, dc_id, server_address, port): self._dc_id = dc_id self._server_address = server_address @@ -82,14 +58,6 @@ class MemorySession(Session): def auth_key(self, value): self._auth_key = value - @property - def time_offset(self): - return self._time_offset - - @time_offset.setter - def time_offset(self, value): - self._time_offset = value - @property def salt(self): return self._salt @@ -143,34 +111,6 @@ class MemorySession(Session): def list_sessions(cls): raise NotImplementedError - def get_new_msg_id(self): - """Generates a new unique message ID based on the current - time (in ms) since epoch""" - now = time.time() + self._time_offset - nanoseconds = int((now - int(now)) * 1e+9) - new_msg_id = (int(now) << 32) | (nanoseconds << 2) - - if self._last_msg_id >= new_msg_id: - new_msg_id = self._last_msg_id + 4 - - self._last_msg_id = new_msg_id - - return new_msg_id - - def update_time_offset(self, correct_msg_id): - now = int(time.time()) - correct = correct_msg_id >> 32 - self._time_offset = correct - now - self._last_msg_id = 0 - - def generate_sequence(self, content_related): - if content_related: - result = self._sequence * 2 + 1 - self._sequence += 1 - return result - else: - return self._sequence * 2 - @staticmethod def _entities_to_rows(tlo): if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): From 4c7224e56aad2b9fc2f1b84ab4404a12b121339f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 2 Mar 2018 10:10:59 +0100 Subject: [PATCH 237/361] Fix n might be None when stopping workers --- telethon/update_state.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index 98345cdf..6a496603 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -71,9 +71,10 @@ class UpdateState: # Put dummy ``None`` objects so that they don't need to timeout. n = self._workers self._workers = None - with self._updates_lock: - for _ in range(n): - self._updates.put(None) + if n: + with self._updates_lock: + for _ in range(n): + self._updates.put(None) for t in self._worker_threads: t.join() From df3faaeb7f4f2694d9e68074fbb91314dad21af7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 11:11:59 +0200 Subject: [PATCH 238/361] Fix abstract Session method ordering --- telethon/sessions/abstract.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index ff0fd16d..ec4f649f 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -52,14 +52,6 @@ class Session(ABC): def auth_key(self, value): raise NotImplementedError - @property - def time_offset(self): - return self._time_offset - - @time_offset.setter - def time_offset(self, value): - self._time_offset = value - @property @abstractmethod def salt(self): @@ -127,6 +119,14 @@ class Session(ABC): def report_errors(self): return self._report_errors + @property + def time_offset(self): + return self._time_offset + + @time_offset.setter + def time_offset(self, value): + self._time_offset = value + @property def flood_sleep_threshold(self): return self._flood_sleep_threshold From d9a73744a49031d823fdd1da671476ddfc56c9d8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 12:36:39 +0200 Subject: [PATCH 239/361] Remove old sqlite session variables and clone code --- telethon/sessions/sqlite.py | 47 +++++++------------------------------ 1 file changed, 9 insertions(+), 38 deletions(-) diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 66a0c887..64f3cbf6 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -32,7 +32,7 @@ class SQLiteSession(MemorySession): through an official Telegram client to revoke the authorization. """ - def __init__(self, session_id): + def __init__(self, session_id=None): super().__init__() """session_user_id should either be a string or another Session. Note that if another session is given, only parameters like @@ -40,51 +40,20 @@ class SQLiteSession(MemorySession): """ # These values will NOT be saved self.filename = ':memory:' + self.save_entities = True - # For connection purposes - if isinstance(session_id, Session): - self._device_model = session_id.device_model - self._system_version = session_id.system_version - self._app_version = session_id.app_version - self._lang_code = session_id.lang_code - self._system_lang_code = session_id.system_lang_code - self._report_errors = session_id.report_errors - self._flood_sleep_threshold = session_id.flood_sleep_threshold - if isinstance(session_id, SQLiteSession): - self.save_entities = session_id.save_entities - else: # str / None - if session_id: - self.filename = session_id - if not self.filename.endswith(EXTENSION): - self.filename += EXTENSION - - system = platform.uname() - self._device_model = system.system or 'Unknown' - self._system_version = system.release or '1.0' - self._app_version = '1.0' # '0' will provoke error - self._lang_code = 'en' - self._system_lang_code = self.lang_code - self._report_errors = True - self.save_entities = True - self._flood_sleep_threshold = 60 + if session_id: + self.filename = session_id + if not self.filename.endswith(EXTENSION): + self.filename += EXTENSION self.id = struct.unpack('q', os.urandom(8))[0] - self._sequence = 0 - self.time_offset = 0 - self._last_msg_id = 0 # Long - self.salt = 0 # Long # Cross-thread safety self._seq_no_lock = Lock() self._msg_id_lock = Lock() self._db_lock = RLock() - # These values will be saved - self._dc_id = 0 - self._server_address = None - self._port = None - self._auth_key = None - # Migrating from .json -> SQL entities = self._check_migrate_json() @@ -152,7 +121,9 @@ class SQLiteSession(MemorySession): self.save() def clone(self): - return SQLiteSession(self) + cloned = super().clone() + cloned.save_entities = self.save_entities + return cloned def _check_migrate_json(self): if file_exists(self.filename): From 118d9b10e869f04c818541b1cea41ccd801fd800 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 13:20:11 +0200 Subject: [PATCH 240/361] Add more abstraction --- telethon/sessions/abstract.py | 5 ++- telethon/sessions/memory.py | 69 +++++++++++++++++++---------------- telethon/sessions/sqlite.py | 23 ++++++------ 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index ec4f649f..89f80b7a 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -18,8 +18,8 @@ class Session(ABC): self._report_errors = True self._flood_sleep_threshold = 60 - def clone(self): - cloned = self.__class__() + def clone(self, to_instance=None): + cloned = to_instance or self.__class__() cloned._device_model = self.device_model cloned._system_version = self.system_version cloned._app_version = self.app_version @@ -27,6 +27,7 @@ class Session(ABC): cloned._system_lang_code = self.system_lang_code cloned._report_errors = self.report_errors cloned._flood_sleep_threshold = self.flood_sleep_threshold + return cloned @abstractmethod def set_dc(self, dc_id, server_address, port): diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 09aa1fa0..92674fa6 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -111,8 +111,41 @@ class MemorySession(Session): def list_sessions(cls): raise NotImplementedError - @staticmethod - def _entities_to_rows(tlo): + def _entity_values_to_row(self, id, hash, username, phone, name): + return id, hash, username, phone, name + + def _entity_to_row(self, e): + if not isinstance(e, TLObject): + return + try: + p = utils.get_input_peer(e, allow_self=False) + marked_id = utils.get_peer_id(p) + except ValueError: + return + + if isinstance(p, (InputPeerUser, InputPeerChannel)): + if not p.access_hash: + # Some users and channels seem to be returned without + # an 'access_hash', meaning Telegram doesn't want you + # to access them. This is the reason behind ensuring + # that the 'access_hash' is non-zero. See issue #354. + # Note that this checks for zero or None, see #392. + return + else: + p_hash = p.access_hash + elif isinstance(p, InputPeerChat): + p_hash = 0 + else: + return + + username = getattr(e, 'username', None) or None + if username is not None: + username = username.lower() + phone = getattr(e, 'phone', None) + name = utils.get_display_name(e) or None + return self._entity_values_to_row(marked_id, p_hash, username, phone, name) + + def _entities_to_rows(self, tlo): if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): # This may be a list of users already for instance entities = tlo @@ -127,35 +160,9 @@ class MemorySession(Session): rows = [] # Rows to add (id, hash, username, phone, name) for e in entities: - if not isinstance(e, TLObject): - continue - try: - p = utils.get_input_peer(e, allow_self=False) - marked_id = utils.get_peer_id(p) - except ValueError: - continue - - if isinstance(p, (InputPeerUser, InputPeerChannel)): - if not p.access_hash: - # Some users and channels seem to be returned without - # an 'access_hash', meaning Telegram doesn't want you - # to access them. This is the reason behind ensuring - # that the 'access_hash' is non-zero. See issue #354. - # Note that this checks for zero or None, see #392. - continue - else: - p_hash = p.access_hash - elif isinstance(p, InputPeerChat): - p_hash = 0 - else: - continue - - username = getattr(e, 'username', None) or None - if username is not None: - username = username.lower() - phone = getattr(e, 'phone', None) - name = utils.get_display_name(e) or None - rows.append((marked_id, p_hash, username, phone, name)) + row = self._entity_to_row(e) + if row: + rows.append(row) return rows def process_entities(self, tlo): diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 64f3cbf6..0ea26ae5 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -120,8 +120,8 @@ class SQLiteSession(MemorySession): c.close() self.save() - def clone(self): - cloned = super().clone() + def clone(self, to_instance=None): + cloned = super().clone(to_instance) cloned.save_entities = self.save_entities return cloned @@ -180,9 +180,7 @@ class SQLiteSession(MemorySession): # Data from sessions should be kept as properties # not to fetch the database every time we need it def set_dc(self, dc_id, server_address, port): - self._dc_id = dc_id - self._server_address = server_address - self._port = port + super().set_dc(dc_id, server_address, port) self._update_session_table() # Fetch the auth_key corresponding to this data center @@ -287,16 +285,19 @@ class SQLiteSession(MemorySession): 'select id, hash from entities where phone=?', (phone,)) def get_entity_rows_by_username(self, username): - self._fetchone_entity('select id, hash from entities where username=?', - (username,)) + return self._fetchone_entity( + 'select id, hash from entities where username=?', + (username,)) def get_entity_rows_by_name(self, name): - self._fetchone_entity('select id, hash from entities where name=?', - (name,)) + return self._fetchone_entity( + 'select id, hash from entities where name=?', + (name,)) def get_entity_rows_by_id(self, id): - self._fetchone_entity('select id, hash from entities where id=?', - (id,)) + return self._fetchone_entity( + 'select id, hash from entities where id=?', + (id,)) # File processing From 07c2fc50ec3f841b9366777bbd0bb63164875c98 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 13:22:30 +0200 Subject: [PATCH 241/361] Add SQLAlchemy-based session --- optional-requirements.txt | 1 + setup.py | 3 +- telethon/sessions/sqlalchemy.py | 177 ++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 telethon/sessions/sqlalchemy.py diff --git a/optional-requirements.txt b/optional-requirements.txt index 55bfc014..fb83c1ab 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,3 +1,4 @@ cryptg pysocks hachoir3 +sqlalchemy diff --git a/setup.py b/setup.py index 00dd7446..0e052d31 100755 --- a/setup.py +++ b/setup.py @@ -151,7 +151,8 @@ def main(): ]), install_requires=['pyaes', 'rsa'], extras_require={ - 'cryptg': ['cryptg'] + 'cryptg': ['cryptg'], + 'sqlalchemy': ['sqlalchemy'] } ) diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py new file mode 100644 index 00000000..94f24a1e --- /dev/null +++ b/telethon/sessions/sqlalchemy.py @@ -0,0 +1,177 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, String, Integer, Blob, orm +import sqlalchemy as sql + +from ..tl.types import InputPhoto, InputDocument + +from .memory import MemorySession, _SentFileType + +Base = declarative_base() +LATEST_VERSION = 1 + + +class DBVersion(Base): + __tablename__ = "version" + version = Column(Integer, primary_key=True) + + +class DBSession(Base): + __tablename__ = "sessions" + + session_id = Column(String, primary_key=True) + dc_id = Column(Integer, primary_key=True) + server_address = Column(String) + port = Column(Integer) + auth_key = Column(Blob) + + +class DBEntity(Base): + __tablename__ = "entities" + + session_id = Column(String, primary_key=True) + id = Column(Integer, primary_key=True) + hash = Column(Integer, nullable=False) + username = Column(String) + phone = Column(Integer) + name = Column(String) + + +class DBSentFile(Base): + __tablename__ = "sent_files" + + session_id = Column(String, primary_key=True) + md5_digest = Column(Blob, primary_key=True) + file_size = Column(Integer, primary_key=True) + type = Column(Integer, primary_key=True) + id = Column(Integer) + hash = Column(Integer) + + +class AlchemySessionContainer: + def __init__(self, database): + if not isinstance(database, sql.Engine): + database = sql.create_engine(database) + + self.db_engine = database + db_factory = orm.sessionmaker(bind=self.db_engine) + self.db = orm.scoping.scoped_session(db_factory) + + if not self.db_engine.dialect.has_table(self.db_engine, + DBVersion.__tablename__): + Base.metadata.create_all(bind=self.db_engine) + self.db.add(DBVersion(version=LATEST_VERSION)) + self.db.commit() + else: + self.check_and_upgrade_database() + + DBVersion.query = self.db.query_property() + DBSession.query = self.db.query_property() + DBEntity.query = self.db.query_property() + DBSentFile.query = self.db.query_property() + + def check_and_upgrade_database(self): + row = DBVersion.query.get() + version = row.version if row else 1 + if version == LATEST_VERSION: + return + + DBVersion.query.delete() + + # Implement table schema updates here and increase version + + self.db.add(DBVersion(version=version)) + self.db.commit() + + def new_session(self, session_id): + return AlchemySession(self, session_id) + + def list_sessions(self): + return + + def save(self): + self.db.commit() + + +class AlchemySession(MemorySession): + def __init__(self, container, session_id): + super().__init__() + self.container = container + self.db = container.db + self.session_id = session_id + + def clone(self, to_instance=None): + cloned = to_instance or self.__class__(self.container, self.session_id) + return super().clone(cloned) + + def set_dc(self, dc_id, server_address, port): + super().set_dc(dc_id, server_address, port) + + def _update_session_table(self): + self.db.query(DBSession).filter( + DBSession.session_id == self.session_id).delete() + new = DBSession(session_id=self.session_id, dc_id=self._dc_id, + server_address=self._server_address, port=self._port, + auth_key=self._auth_key.key if self._auth_key else b'') + self.db.merge(new) + + def _db_query(self, dbclass, *args): + return self.db.query(dbclass).filter( + dbclass.session_id == self.session_id, + *args) + + def save(self): + self.container.save() + + def close(self): + # Nothing to do here, connection is managed by AlchemySessionContainer. + pass + + def delete(self): + self._db_query(DBSession).delete() + self._db_query(DBEntity).delete() + self._db_query(DBSentFile).delete() + + def _entity_values_to_row(self, id, hash, username, phone, name): + return DBEntity(session_id=self.session_id, id=id, hash=hash, + username=username, phone=phone, name=name) + + def process_entities(self, tlo): + rows = self._entities_to_rows(tlo) + if not rows: + return + + self.db.add_all(rows) + self.save() + + def get_entity_rows_by_phone(self, key): + row = self._db_query(DBEntity, DBEntity.phone == key).one_or_none() + return row.id, row.hash if row else None + + def get_entity_rows_by_username(self, key): + row = self._db_query(DBEntity, DBEntity.username == key).one_or_none() + return row.id, row.hash if row else None + + def get_entity_rows_by_name(self, key): + row = self._db_query(DBEntity, DBEntity.name == key).one_or_none() + return row.id, row.hash if row else None + + def get_entity_rows_by_id(self, key): + row = self._db_query(DBEntity, DBEntity.id == key).one_or_none() + return row.id, row.hash if row else None + + def get_file(self, md5_digest, file_size, cls): + row = self._db_query(DBSentFile, DBSentFile.md5_digest == md5_digest, + DBSentFile.file_size == file_size, + DBSentFile.type == _SentFileType.from_type( + cls).value).one_or_none() + return row.id, row.hash if row else None + + def cache_file(self, md5_digest, file_size, instance): + if not isinstance(instance, (InputDocument, InputPhoto)): + raise TypeError('Cannot cache %s instance' % type(instance)) + + self.db.merge( + DBSentFile(session_id=self.session_id, md5_digest=md5_digest, + type=_SentFileType.from_type(type(instance)).value, + id=instance.id, hash=instance.access_hash)) + self.save() From 03d4ab37657123e4ba3258ccf1867e645630b111 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 13:25:40 +0200 Subject: [PATCH 242/361] Fix create_engine check --- telethon/sessions/sqlalchemy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py index 94f24a1e..782f811f 100644 --- a/telethon/sessions/sqlalchemy.py +++ b/telethon/sessions/sqlalchemy.py @@ -49,7 +49,7 @@ class DBSentFile(Base): class AlchemySessionContainer: def __init__(self, database): - if not isinstance(database, sql.Engine): + if isinstance(database, str): database = sql.create_engine(database) self.db_engine = database From e1d7cc541f878a22a46dbd3f8cc2d0dd5115f16d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 18:23:12 +0200 Subject: [PATCH 243/361] Add setters for non-persistent values that apps might change --- telethon/sessions/abstract.py | 32 ++++++++++++++++++++++++++++++++ telethon/sessions/memory.py | 32 -------------------------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index 89f80b7a..dd1541ab 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -100,26 +100,50 @@ class Session(ABC): def device_model(self): return self._device_model + @device_model.setter + def device_model(self, value): + self._device_model = value + @property def system_version(self): return self._system_version + @system_version.setter + def system_version(self, value): + self._system_version = value + @property def app_version(self): return self._app_version + @app_version.setter + def app_version(self, value): + self._app_version = value + @property def lang_code(self): return self._lang_code + @lang_code.setter + def lang_code(self, value): + self._lang_code = value + @property def system_lang_code(self): return self._system_lang_code + @system_lang_code.setter + def system_lang_code(self, value): + self._system_lang_code = value + @property def report_errors(self): return self._report_errors + @report_errors.setter + def report_errors(self, value): + self._report_errors = value + @property def time_offset(self): return self._time_offset @@ -132,10 +156,18 @@ class Session(ABC): def flood_sleep_threshold(self): return self._flood_sleep_threshold + @flood_sleep_threshold.setter + def flood_sleep_threshold(self, value): + self._flood_sleep_threshold = value + @property def sequence(self): return self._sequence + @sequence.setter + def sequence(self, value): + self._sequence = value + def get_new_msg_id(self): """Generates a new unique message ID based on the current time (in ms) since epoch""" diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 92674fa6..71d6e551 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -66,38 +66,6 @@ class MemorySession(Session): def salt(self, value): self._salt = value - @property - def device_model(self): - return self._device_model - - @property - def system_version(self): - return self._system_version - - @property - def app_version(self): - return self._app_version - - @property - def lang_code(self): - return self._lang_code - - @property - def system_lang_code(self): - return self._system_lang_code - - @property - def report_errors(self): - return self._report_errors - - @property - def sequence(self): - return self._sequence - - @property - def flood_sleep_threshold(self): - return self._flood_sleep_threshold - def close(self): pass From a7f98fd3cc2abdd6935eef69c5a8251b0756ca4a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 2 Mar 2018 17:26:42 +0100 Subject: [PATCH 244/361] Ignore bad file descriptor while closing --- telethon/extensions/tcp_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index d9cea2f0..d335e57a 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -168,7 +168,10 @@ class TcpClient: __log__.info('ConnectionError "%s" while reading data', e) self._raise_connection_reset(e) except OSError as e: - __log__.info('OSError "%s" while reading data', e) + if e.errno != errno.EBADF and self._closing_lock.locked(): + # Ignore bad file descriptor while closing + __log__.info('OSError "%s" while reading data', e) + if e.errno in CONN_RESET_ERRNOS: self._raise_connection_reset(e) else: From dc2229fdba10f0140a7e159737091d82852cdef3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 18:39:04 +0200 Subject: [PATCH 245/361] Move salt and ID to base session and remove unused imports --- telethon/sessions/abstract.py | 23 +++++++++++++---------- telethon/sessions/memory.py | 10 +--------- telethon/sessions/sqlite.py | 12 +----------- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index dd1541ab..d92e0754 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -1,13 +1,18 @@ from abc import ABC, abstractmethod import time import platform +import struct +import os class Session(ABC): def __init__(self): + self.id = struct.unpack('q', os.urandom(8))[0] + self._sequence = 0 self._last_msg_id = 0 self._time_offset = 0 + self._salt = 0 system = platform.uname() self._device_model = system.system or 'Unknown' @@ -53,16 +58,6 @@ class Session(ABC): def auth_key(self, value): raise NotImplementedError - @property - @abstractmethod - def salt(self): - raise NotImplementedError - - @salt.setter - @abstractmethod - def salt(self, value): - raise NotImplementedError - @abstractmethod def close(self): raise NotImplementedError @@ -96,6 +91,14 @@ class Session(ABC): def get_file(self, md5_digest, file_size, cls): raise NotImplementedError + @property + def salt(self): + return self._salt + + @salt.setter + def salt(self, value): + self._salt = value + @property def device_model(self): return self._device_model diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 71d6e551..7ab31b21 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -28,10 +28,10 @@ class _SentFileType(Enum): class MemorySession(Session): def __init__(self): super().__init__() + self._dc_id = None self._server_address = None self._port = None - self._salt = None self._auth_key = None self._files = {} @@ -58,14 +58,6 @@ class MemorySession(Session): def auth_key(self, value): self._auth_key = value - @property - def salt(self): - return self._salt - - @salt.setter - def salt(self, value): - self._salt = value - def close(self): pass diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 0ea26ae5..0423d88a 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -1,21 +1,13 @@ import json import os -import platform import sqlite3 -import struct -import time from base64 import b64decode from os.path import isfile as file_exists from threading import Lock, RLock -from .. import utils -from .abstract import Session from .memory import MemorySession, _SentFileType from ..crypto import AuthKey -from ..tl import TLObject from ..tl.types import ( - PeerUser, PeerChat, PeerChannel, - InputPeerUser, InputPeerChat, InputPeerChannel, InputPhoto, InputDocument ) @@ -47,8 +39,6 @@ class SQLiteSession(MemorySession): if not self.filename.endswith(EXTENSION): self.filename += EXTENSION - self.id = struct.unpack('q', os.urandom(8))[0] - # Cross-thread safety self._seq_no_lock = Lock() self._msg_id_lock = Lock() @@ -193,7 +183,7 @@ class SQLiteSession(MemorySession): self._auth_key = None c.close() - @Session.auth_key.setter + @MemorySession.auth_key.setter def auth_key(self, value): self._auth_key = value self._update_session_table() From c1a8896faa43dfa798fc9223283eae42dbf5afd6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 20:14:11 +0200 Subject: [PATCH 246/361] Fix SQLAlchemy implementation --- telethon/sessions/__init__.py | 1 + telethon/sessions/sqlalchemy.py | 197 ++++++++++++++++++-------------- 2 files changed, 114 insertions(+), 84 deletions(-) diff --git a/telethon/sessions/__init__.py b/telethon/sessions/__init__.py index af3423f3..a487a4bd 100644 --- a/telethon/sessions/__init__.py +++ b/telethon/sessions/__init__.py @@ -1,3 +1,4 @@ from .abstract import Session from .memory import MemorySession from .sqlite import SQLiteSession +from .sqlalchemy import AlchemySessionContainer, AlchemySession diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py index 782f811f..aa618e4c 100644 --- a/telethon/sessions/sqlalchemy.py +++ b/telethon/sessions/sqlalchemy.py @@ -1,85 +1,95 @@ from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import Column, String, Integer, Blob, orm +from sqlalchemy import Column, String, Integer, BLOB, orm import sqlalchemy as sql +from ..crypto import AuthKey from ..tl.types import InputPhoto, InputDocument from .memory import MemorySession, _SentFileType -Base = declarative_base() LATEST_VERSION = 1 -class DBVersion(Base): - __tablename__ = "version" - version = Column(Integer, primary_key=True) - - -class DBSession(Base): - __tablename__ = "sessions" - - session_id = Column(String, primary_key=True) - dc_id = Column(Integer, primary_key=True) - server_address = Column(String) - port = Column(Integer) - auth_key = Column(Blob) - - -class DBEntity(Base): - __tablename__ = "entities" - - session_id = Column(String, primary_key=True) - id = Column(Integer, primary_key=True) - hash = Column(Integer, nullable=False) - username = Column(String) - phone = Column(Integer) - name = Column(String) - - -class DBSentFile(Base): - __tablename__ = "sent_files" - - session_id = Column(String, primary_key=True) - md5_digest = Column(Blob, primary_key=True) - file_size = Column(Integer, primary_key=True) - type = Column(Integer, primary_key=True) - id = Column(Integer) - hash = Column(Integer) - - class AlchemySessionContainer: - def __init__(self, database): - if isinstance(database, str): - database = sql.create_engine(database) + def __init__(self, engine=None, session=None, table_prefix="", + table_base=None, manage_tables=True): + if isinstance(engine, str): + engine = sql.create_engine(engine) - self.db_engine = database - db_factory = orm.sessionmaker(bind=self.db_engine) - self.db = orm.scoping.scoped_session(db_factory) - - if not self.db_engine.dialect.has_table(self.db_engine, - DBVersion.__tablename__): - Base.metadata.create_all(bind=self.db_engine) - self.db.add(DBVersion(version=LATEST_VERSION)) - self.db.commit() + self.db_engine = engine + if not session: + db_factory = orm.sessionmaker(bind=self.db_engine) + self.db = orm.scoping.scoped_session(db_factory) else: - self.check_and_upgrade_database() + self.db = session - DBVersion.query = self.db.query_property() - DBSession.query = self.db.query_property() - DBEntity.query = self.db.query_property() - DBSentFile.query = self.db.query_property() + table_base = table_base or declarative_base() + (self.Version, self.Session, self.Entity, + self.SentFile) = self.create_table_classes(self.db, table_prefix, + table_base) + + if manage_tables: + table_base.metadata.bind = self.db_engine + if not self.db_engine.dialect.has_table(self.db_engine, + self.Version.__tablename__): + table_base.metadata.create_all() + self.db.add(self.Version(version=LATEST_VERSION)) + self.db.commit() + else: + self.check_and_upgrade_database() + + @staticmethod + def create_table_classes(db, prefix, Base): + class Version(Base): + query = db.query_property() + __tablename__ = "{prefix}version".format(prefix=prefix) + version = Column(Integer, primary_key=True) + + class Session(Base): + query = db.query_property() + __tablename__ = "{prefix}sessions".format(prefix=prefix) + + session_id = Column(String, primary_key=True) + dc_id = Column(Integer, primary_key=True) + server_address = Column(String) + port = Column(Integer) + auth_key = Column(BLOB) + + class Entity(Base): + query = db.query_property() + __tablename__ = "{prefix}entities".format(prefix=prefix) + + session_id = Column(String, primary_key=True) + id = Column(Integer, primary_key=True) + hash = Column(Integer, nullable=False) + username = Column(String) + phone = Column(Integer) + name = Column(String) + + class SentFile(Base): + query = db.query_property() + __tablename__ = "{prefix}sent_files".format(prefix=prefix) + + session_id = Column(String, primary_key=True) + md5_digest = Column(BLOB, primary_key=True) + file_size = Column(Integer, primary_key=True) + type = Column(Integer, primary_key=True) + id = Column(Integer) + hash = Column(Integer) + + return Version, Session, Entity, SentFile def check_and_upgrade_database(self): - row = DBVersion.query.get() - version = row.version if row else 1 + row = self.Version.query.all() + version = row[0].version if row else 1 if version == LATEST_VERSION: return - DBVersion.query.delete() + self.Version.query.delete() # Implement table schema updates here and increase version - self.db.add(DBVersion(version=version)) + self.db.add(self.Version(version=version)) self.db.commit() def new_session(self, session_id): @@ -97,7 +107,20 @@ class AlchemySession(MemorySession): super().__init__() self.container = container self.db = container.db + self.Version, self.Session, self.Entity, self.SentFile = ( + container.Version, container.Session, container.Entity, + container.SentFile) self.session_id = session_id + self.load_session() + + def load_session(self): + sessions = self._db_query(self.Session).all() + session = sessions[0] if sessions else None + if session: + self._dc_id = session.dc_id + self._server_address = session.server_address + self._port = session.port + self._auth_key = AuthKey(data=session.auth_key) def clone(self, to_instance=None): cloned = to_instance or self.__class__(self.container, self.session_id) @@ -107,17 +130,18 @@ class AlchemySession(MemorySession): super().set_dc(dc_id, server_address, port) def _update_session_table(self): - self.db.query(DBSession).filter( - DBSession.session_id == self.session_id).delete() - new = DBSession(session_id=self.session_id, dc_id=self._dc_id, - server_address=self._server_address, port=self._port, - auth_key=self._auth_key.key if self._auth_key else b'') + self.Session.query.filter( + self.Session.session_id == self.session_id).delete() + new = self.Session(session_id=self.session_id, dc_id=self._dc_id, + server_address=self._server_address, + port=self._port, + auth_key=(self._auth_key.key + if self._auth_key else b'')) self.db.merge(new) def _db_query(self, dbclass, *args): - return self.db.query(dbclass).filter( - dbclass.session_id == self.session_id, - *args) + return dbclass.query.filter(dbclass.session_id == self.session_id, + *args) def save(self): self.container.save() @@ -127,42 +151,47 @@ class AlchemySession(MemorySession): pass def delete(self): - self._db_query(DBSession).delete() - self._db_query(DBEntity).delete() - self._db_query(DBSentFile).delete() + self._db_query(self.Session).delete() + self._db_query(self.Entity).delete() + self._db_query(self.SentFile).delete() def _entity_values_to_row(self, id, hash, username, phone, name): - return DBEntity(session_id=self.session_id, id=id, hash=hash, - username=username, phone=phone, name=name) + return self.Entity(session_id=self.session_id, id=id, hash=hash, + username=username, phone=phone, name=name) def process_entities(self, tlo): rows = self._entities_to_rows(tlo) if not rows: return - self.db.add_all(rows) + for row in rows: + self.db.merge(row) self.save() def get_entity_rows_by_phone(self, key): - row = self._db_query(DBEntity, DBEntity.phone == key).one_or_none() + row = self._db_query(self.Entity, + self.Entity.phone == key).one_or_none() return row.id, row.hash if row else None def get_entity_rows_by_username(self, key): - row = self._db_query(DBEntity, DBEntity.username == key).one_or_none() + row = self._db_query(self.Entity, + self.Entity.username == key).one_or_none() return row.id, row.hash if row else None def get_entity_rows_by_name(self, key): - row = self._db_query(DBEntity, DBEntity.name == key).one_or_none() + row = self._db_query(self.Entity, + self.Entity.name == key).one_or_none() return row.id, row.hash if row else None def get_entity_rows_by_id(self, key): - row = self._db_query(DBEntity, DBEntity.id == key).one_or_none() + row = self._db_query(self.Entity, self.Entity.id == key).one_or_none() return row.id, row.hash if row else None def get_file(self, md5_digest, file_size, cls): - row = self._db_query(DBSentFile, DBSentFile.md5_digest == md5_digest, - DBSentFile.file_size == file_size, - DBSentFile.type == _SentFileType.from_type( + row = self._db_query(self.SentFile, + self.SentFile.md5_digest == md5_digest, + self.SentFile.file_size == file_size, + self.SentFile.type == _SentFileType.from_type( cls).value).one_or_none() return row.id, row.hash if row else None @@ -171,7 +200,7 @@ class AlchemySession(MemorySession): raise TypeError('Cannot cache %s instance' % type(instance)) self.db.merge( - DBSentFile(session_id=self.session_id, md5_digest=md5_digest, - type=_SentFileType.from_type(type(instance)).value, - id=instance.id, hash=instance.access_hash)) + self.SentFile(session_id=self.session_id, md5_digest=md5_digest, + type=_SentFileType.from_type(type(instance)).value, + id=instance.id, hash=instance.access_hash)) self.save() From f805914c80e034058789ec6d7fef43dd4762e051 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 20:40:03 +0200 Subject: [PATCH 247/361] Handle SQLAlchemy import errors --- telethon/sessions/sqlalchemy.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py index aa618e4c..0b028c02 100644 --- a/telethon/sessions/sqlalchemy.py +++ b/telethon/sessions/sqlalchemy.py @@ -1,6 +1,10 @@ -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import Column, String, Integer, BLOB, orm -import sqlalchemy as sql +try: + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy import Column, String, Integer, BLOB, orm + import sqlalchemy as sql +except ImportError: + sql = None + pass from ..crypto import AuthKey from ..tl.types import InputPhoto, InputDocument @@ -13,6 +17,8 @@ LATEST_VERSION = 1 class AlchemySessionContainer: def __init__(self, engine=None, session=None, table_prefix="", table_base=None, manage_tables=True): + if not sql: + raise ImportError("SQLAlchemy not imported") if isinstance(engine, str): engine = sql.create_engine(engine) From 5e88b21aa9ce5bd51ecd7fcd3627797c58d996ee Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 20:42:51 +0200 Subject: [PATCH 248/361] Use single quotes --- telethon/sessions/sqlalchemy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py index 0b028c02..0fd76fe3 100644 --- a/telethon/sessions/sqlalchemy.py +++ b/telethon/sessions/sqlalchemy.py @@ -15,10 +15,10 @@ LATEST_VERSION = 1 class AlchemySessionContainer: - def __init__(self, engine=None, session=None, table_prefix="", + def __init__(self, engine=None, session=None, table_prefix='', table_base=None, manage_tables=True): if not sql: - raise ImportError("SQLAlchemy not imported") + raise ImportError('SQLAlchemy not imported') if isinstance(engine, str): engine = sql.create_engine(engine) @@ -48,12 +48,12 @@ class AlchemySessionContainer: def create_table_classes(db, prefix, Base): class Version(Base): query = db.query_property() - __tablename__ = "{prefix}version".format(prefix=prefix) + __tablename__ = '{prefix}version'.format(prefix=prefix) version = Column(Integer, primary_key=True) class Session(Base): query = db.query_property() - __tablename__ = "{prefix}sessions".format(prefix=prefix) + __tablename__ = '{prefix}sessions'.format(prefix=prefix) session_id = Column(String, primary_key=True) dc_id = Column(Integer, primary_key=True) @@ -63,7 +63,7 @@ class AlchemySessionContainer: class Entity(Base): query = db.query_property() - __tablename__ = "{prefix}entities".format(prefix=prefix) + __tablename__ = '{prefix}entities'.format(prefix=prefix) session_id = Column(String, primary_key=True) id = Column(Integer, primary_key=True) @@ -74,7 +74,7 @@ class AlchemySessionContainer: class SentFile(Base): query = db.query_property() - __tablename__ = "{prefix}sent_files".format(prefix=prefix) + __tablename__ = '{prefix}sent_files'.format(prefix=prefix) session_id = Column(String, primary_key=True) md5_digest = Column(BLOB, primary_key=True) From 47cdcda9e2b8c17d3f4e842509a67fcf76b2548c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 21:05:09 +0200 Subject: [PATCH 249/361] Move device info out of Session --- telethon/sessions/abstract.py | 53 -------------------------------- telethon/telegram_bare_client.py | 30 ++++++++++-------- 2 files changed, 18 insertions(+), 65 deletions(-) diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index d92e0754..647a87c1 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod import time -import platform import struct import os @@ -13,23 +12,11 @@ class Session(ABC): self._last_msg_id = 0 self._time_offset = 0 self._salt = 0 - - system = platform.uname() - self._device_model = system.system or 'Unknown' - self._system_version = system.release or '1.0' - self._app_version = '1.0' - self._lang_code = 'en' - self._system_lang_code = self.lang_code self._report_errors = True self._flood_sleep_threshold = 60 def clone(self, to_instance=None): cloned = to_instance or self.__class__() - cloned._device_model = self.device_model - cloned._system_version = self.system_version - cloned._app_version = self.app_version - cloned._lang_code = self.lang_code - cloned._system_lang_code = self.system_lang_code cloned._report_errors = self.report_errors cloned._flood_sleep_threshold = self.flood_sleep_threshold return cloned @@ -99,46 +86,6 @@ class Session(ABC): def salt(self, value): self._salt = value - @property - def device_model(self): - return self._device_model - - @device_model.setter - def device_model(self, value): - self._device_model = value - - @property - def system_version(self): - return self._system_version - - @system_version.setter - def system_version(self, value): - self._system_version = value - - @property - def app_version(self): - return self._app_version - - @app_version.setter - def app_version(self, value): - self._app_version = value - - @property - def lang_code(self): - return self._lang_code - - @lang_code.setter - def lang_code(self, value): - self._lang_code = value - - @property - def system_lang_code(self): - return self._system_lang_code - - @system_lang_code.setter - def system_lang_code(self, value): - self._system_lang_code = value - @property def report_errors(self): return self._report_errors diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 3a5b2bd0..bf33a7dc 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -1,11 +1,11 @@ import logging import os +import platform import threading from datetime import timedelta, datetime from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Lock from time import sleep - from . import version, utils from .crypto import rsa from .errors import ( @@ -73,7 +73,12 @@ class TelegramBareClient: update_workers=None, spawn_read_thread=False, timeout=timedelta(seconds=5), - **kwargs): + loop=None, + device_model=None, + system_version=None, + app_version=None, + lang_code='en', + system_lang_code='en'): """Refer to TelegramClient.__init__ for docs on this method""" if not api_id or not api_hash: raise ValueError( @@ -125,11 +130,12 @@ class TelegramBareClient: self.updates = UpdateState(workers=update_workers) # Used on connection - the user may modify these and reconnect - kwargs['app_version'] = kwargs.get('app_version', self.__version__) - for name, value in kwargs.items(): - if not hasattr(self.session, name): - raise ValueError('Unknown named parameter', name) - setattr(self.session, name, value) + system = platform.uname() + self.device_model = device_model or system.system or 'Unknown' + self.system_version = system_version or system.release or '1.0' + self.app_version = app_version or self.__version__ + self.lang_code = lang_code + self.system_lang_code = system_lang_code # Despite the state of the real connection, keep track of whether # the user has explicitly called .connect() or .disconnect() here. @@ -233,11 +239,11 @@ class TelegramBareClient: """Wraps query around InvokeWithLayerRequest(InitConnectionRequest())""" return InvokeWithLayerRequest(LAYER, InitConnectionRequest( api_id=self.api_id, - device_model=self.session.device_model, - system_version=self.session.system_version, - app_version=self.session.app_version, - lang_code=self.session.lang_code, - system_lang_code=self.session.system_lang_code, + device_model=self.device_model, + system_version=self.system_version, + app_version=self.app_version, + lang_code=self.lang_code, + system_lang_code=self.system_lang_code, lang_pack='', # "langPacks are for official apps only" query=query )) From 290afd85fc5f91688e4e18355ac207e8a2c2f1ec Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Mar 2018 21:58:16 +0200 Subject: [PATCH 250/361] Fix AlchemySession session table updating --- telethon/sessions/sqlalchemy.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py index 0fd76fe3..933f44c2 100644 --- a/telethon/sessions/sqlalchemy.py +++ b/telethon/sessions/sqlalchemy.py @@ -117,9 +117,9 @@ class AlchemySession(MemorySession): container.Version, container.Session, container.Entity, container.SentFile) self.session_id = session_id - self.load_session() + self._load_session() - def load_session(self): + def _load_session(self): sessions = self._db_query(self.Session).all() session = sessions[0] if sessions else None if session: @@ -134,6 +134,19 @@ class AlchemySession(MemorySession): def set_dc(self, dc_id, server_address, port): super().set_dc(dc_id, server_address, port) + self._update_session_table() + + sessions = self._db_query(self.Session).all() + session = sessions[0] if sessions else None + if session and session.auth_key: + self._auth_key = AuthKey(data=session.auth_key) + else: + self._auth_key = None + + @MemorySession.auth_key.setter + def auth_key(self, value): + self._auth_key = value + self._update_session_table() def _update_session_table(self): self.Session.query.filter( From 2e31a686e86699387e2875804f732b5cb513f368 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 2 Mar 2018 21:28:33 +0100 Subject: [PATCH 251/361] Upgrade to layer 75 Captions are now "messages" and also support message entities. --- .../extra/examples/working-with-messages.rst | 3 +- telethon/telegram_client.py | 36 +++++++++++++------ telethon/utils.py | 24 ++++--------- .../interactive_telegram_client.py | 4 +-- telethon_generator/scheme.tl | 36 +++++++++---------- 5 files changed, 52 insertions(+), 51 deletions(-) diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index ab38788c..e2471a25 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -114,8 +114,7 @@ send yourself the very first sticker you have: id=InputDocument( id=stickers.documents[0].id, access_hash=stickers.documents[0].access_hash - ), - caption='' + ) ) )) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9690fcb4..24615ace 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1128,6 +1128,7 @@ class TelegramClient(TelegramBareClient): attributes=None, thumb=None, allow_cache=True, + parse_mode='md', **kwargs): """ Sends a file to the specified entity. @@ -1177,6 +1178,9 @@ class TelegramClient(TelegramBareClient): Must be ``False`` if you wish to use different attributes or thumb than those that were used when the file was cached. + parse_mode (:obj:`str`, optional): + The parse mode for the caption message. + Kwargs: 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. @@ -1210,18 +1214,21 @@ class TelegramClient(TelegramBareClient): entity = self.get_input_entity(entity) reply_to = self._get_message_id(reply_to) + caption, msg_entities = self._parse_message_text(caption, parse_mode) if not isinstance(file, (str, bytes, io.IOBase)): # The user may pass a Message containing media (or the media, # or anything similar) that should be treated as a file. Try # getting the input media for whatever they passed and send it. try: - media = utils.get_input_media(file, user_caption=caption) + media = utils.get_input_media(file) except TypeError: pass # Can't turn whatever was given into media else: request = SendMediaRequest(entity, media, - reply_to_msg_id=reply_to) + reply_to_msg_id=reply_to, + message=caption, + entities=msg_entities) return self._get_response_message(request, self(request)) as_image = utils.is_image(file) and not force_document @@ -1234,11 +1241,11 @@ class TelegramClient(TelegramBareClient): if isinstance(file_handle, use_cache): # File was cached, so an instance of use_cache was returned if as_image: - media = InputMediaPhoto(file_handle, caption or '') + media = InputMediaPhoto(file_handle) else: - media = InputMediaDocument(file_handle, caption or '') + media = InputMediaDocument(file_handle) elif as_image: - media = InputMediaUploadedPhoto(file_handle, caption or '') + media = InputMediaUploadedPhoto(file_handle) else: mime_type = None if isinstance(file, str): @@ -1309,13 +1316,13 @@ class TelegramClient(TelegramBareClient): file=file_handle, mime_type=mime_type, attributes=list(attr_dict.values()), - caption=caption or '', **input_kw ) # Once the media type is properly specified and the file uploaded, # send the media message to the desired entity. - request = SendMediaRequest(entity, media, reply_to_msg_id=reply_to) + request = SendMediaRequest(entity, media, reply_to_msg_id=reply_to, + message=caption, entities=msg_entities) msg = self._get_response_message(request, self(request)) if msg and isinstance(file_handle, InputSizedFile): # There was a response message and we didn't use cached @@ -1335,15 +1342,18 @@ class TelegramClient(TelegramBareClient): return self.send_file(*args, **kwargs) def _send_album(self, entity, files, caption=None, - progress_callback=None, reply_to=None): + progress_callback=None, reply_to=None, + parse_mode='md'): """Specialized version of .send_file for albums""" # We don't care if the user wants to avoid cache, we will use it # anyway. Why? The cached version will be exactly the same thing # we need to produce right now to send albums (uploadMedia), and # cache only makes a difference for documents where the user may - # want the attributes used on them to change. Caption's ignored. + # want the attributes used on them to change. + # TODO Support a different captions for each file entity = self.get_input_entity(entity) caption = caption or '' + caption, msg_entities = self._parse_message_text(caption, parse_mode) reply_to = self._get_message_id(reply_to) # Need to upload the media first, but only if they're not cached yet @@ -1353,11 +1363,15 @@ class TelegramClient(TelegramBareClient): fh = self.upload_file(file, use_cache=InputPhoto) if not isinstance(fh, InputPhoto): input_photo = utils.get_input_photo(self(UploadMediaRequest( - entity, media=InputMediaUploadedPhoto(fh, caption) + entity, media=InputMediaUploadedPhoto(fh) )).photo) self.session.cache_file(fh.md5, fh.size, input_photo) fh = input_photo - media.append(InputSingleMedia(InputMediaPhoto(fh, caption))) + media.append(InputSingleMedia( + InputMediaPhoto(fh), + message=caption, + entities=msg_entities + )) # Now we can construct the multi-media request result = self(SendMultiMediaRequest( diff --git a/telethon/utils.py b/telethon/utils.py index ed93bdfa..faf69649 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -241,7 +241,7 @@ def get_input_geo(geo): _raise_cast_fail(geo, 'InputGeoPoint') -def get_input_media(media, user_caption=None, is_photo=False): +def get_input_media(media, is_photo=False): """Similar to get_input_peer, but for media. If the media is a file location and is_photo is known to be True, @@ -256,31 +256,23 @@ def get_input_media(media, user_caption=None, is_photo=False): if isinstance(media, MessageMediaPhoto): return InputMediaPhoto( id=get_input_photo(media.photo), - ttl_seconds=media.ttl_seconds, - caption=((media.caption if user_caption is None else user_caption) - or '') + ttl_seconds=media.ttl_seconds ) if isinstance(media, MessageMediaDocument): return InputMediaDocument( id=get_input_document(media.document), - ttl_seconds=media.ttl_seconds, - caption=((media.caption if user_caption is None else user_caption) - or '') + ttl_seconds=media.ttl_seconds ) if isinstance(media, FileLocation): if is_photo: - return InputMediaUploadedPhoto( - file=media, - caption=user_caption or '' - ) + return InputMediaUploadedPhoto(file=media) else: return InputMediaUploadedDocument( file=media, mime_type='application/octet-stream', # unknown, assume bytes - attributes=[DocumentAttributeFilename('unnamed')], - caption=user_caption or '' + attributes=[DocumentAttributeFilename('unnamed')] ) if isinstance(media, MessageMediaGame): @@ -291,7 +283,7 @@ def get_input_media(media, user_caption=None, is_photo=False): media = media.photo_small else: media = media.photo_big - return get_input_media(media, user_caption=user_caption, is_photo=True) + return get_input_media(media, is_photo=True) if isinstance(media, MessageMediaContact): return InputMediaContact( @@ -319,9 +311,7 @@ def get_input_media(media, user_caption=None, is_photo=False): return InputMediaEmpty() if isinstance(media, Message): - return get_input_media( - media.media, user_caption=user_caption, is_photo=is_photo - ) + return get_input_media(media.media, is_photo=is_photo) _raise_cast_fail(media, 'InputMedia') diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index f6986370..44185995 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -222,10 +222,8 @@ class InteractiveTelegramClient(TelegramClient): # Format the message content if getattr(msg, 'media', None): self.found_media[msg.id] = msg - # The media may or may not have a caption - caption = getattr(msg.media, 'caption', '') content = '<{}> {}'.format( - type(msg.media).__name__, caption) + type(msg.media).__name__, msg.message) elif hasattr(msg, 'message'): content = msg.message diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index 491f0c9e..a736b066 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -158,16 +158,16 @@ inputFile#f52ff27f id:long parts:int name:string md5_checksum:string = InputFile inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile; inputMediaEmpty#9664f57f = InputMedia; -inputMediaUploadedPhoto#2f37e231 flags:# file:InputFile caption:string stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; -inputMediaPhoto#81fa373a flags:# id:InputPhoto caption:string ttl_seconds:flags.0?int = InputMedia; +inputMediaUploadedPhoto#1e287d04 flags:# file:InputFile stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; +inputMediaPhoto#b3ba0635 flags:# id:InputPhoto ttl_seconds:flags.0?int = InputMedia; inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia; -inputMediaUploadedDocument#e39621fd flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector caption:string stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; -inputMediaDocument#5acb668e flags:# id:InputDocument caption:string ttl_seconds:flags.0?int = InputMedia; +inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; +inputMediaDocument#23ab23d2 flags:# id:InputDocument ttl_seconds:flags.0?int = InputMedia; inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia; inputMediaGifExternal#4843b0fd url:string q:string = InputMedia; -inputMediaPhotoExternal#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; -inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; +inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia; +inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia; inputMediaGeoLive#7b1a118f geo_point:InputGeoPoint period:int = InputMedia; @@ -243,11 +243,11 @@ message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:fl messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message; messageMediaEmpty#3ded6320 = MessageMedia; -messageMediaPhoto#b5223b0f flags:# photo:flags.0?Photo caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia; +messageMediaPhoto#695150d7 flags:# photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia; messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:string user_id:int = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia; -messageMediaDocument#7c4414d3 flags:# document:flags.0?Document caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia; +messageMediaDocument#9cb070d7 flags:# document:flags.0?Document ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; @@ -688,7 +688,7 @@ messages.foundGifs#450a1c0a next_offset:int results:Vector = messages. messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs; messages.savedGifs#2e0709a5 hash:int gifs:Vector = messages.SavedGifs; -inputBotInlineMessageMediaAuto#292fed13 flags:# caption:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaAuto#3380c786 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaGeo#c1b15d65 flags:# geo_point:InputGeoPoint period:int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaVenue#aaafadc8 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; @@ -700,7 +700,7 @@ inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_m inputBotInlineResultDocument#fff8fdc4 flags:# id:string type:string title:flags.1?string description:flags.2?string document:InputDocument send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:InputBotInlineMessage = InputBotInlineResult; -botInlineMessageMediaAuto#a74b15b flags:# caption:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageMediaAuto#764cf810 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaVenue#4366232e flags:# geo:GeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; @@ -711,7 +711,7 @@ botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector cache_time:int users:Vector = messages.BotResults; -exportedMessageLink#1f486803 link:string = ExportedMessageLink; +exportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink; messageFwdHeader#559ebe6d flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int = MessageFwdHeader; @@ -896,7 +896,7 @@ langPackDifference#f385c1f6 lang_code:string from_version:int version:int string langPackLanguage#117698f1 name:string native_name:string lang_code:string = LangPackLanguage; -channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true manage_call:flags.10?true = ChannelAdminRights; +channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true = ChannelAdminRights; channelBannedRights#58cf4249 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true until_date:int = ChannelBannedRights; @@ -938,7 +938,7 @@ recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; -inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia; +inputSingleMedia#31bc3d25 media:InputMedia flags:# random_id:long message:string entities:flags.0?Vector = InputSingleMedia; ---functions--- @@ -966,7 +966,7 @@ auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentC auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool; -account.registerDevice#f75874d1 token_type:int token:string other_uids:Vector = Bool; +account.registerDevice#1389cc token_type:int token:string app_sandbox:Bool other_uids:Vector = Bool; account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector = Bool; account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool; account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings; @@ -1023,7 +1023,7 @@ messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = me messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#a3825e50 peer:InputPeer action:SendMessageAction = Bool; messages.sendMessage#fa88427a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; -messages.sendMedia#c8f16791 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia random_id:long reply_markup:flags.2?ReplyMarkup = Updates; +messages.sendMedia#b8d1262b flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true grouped:flags.9?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.hideReportSpam#a8f1709b peer:InputPeer = Bool; @@ -1035,7 +1035,6 @@ messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates; messages.addChatUser#f9a0aa09 chat_id:int user_id:InputUser fwd_limit:int = Updates; messages.deleteChatUser#e0611f16 chat_id:int user_id:InputUser = Updates; messages.createChat#9cb126e users:Vector title:string = Updates; -messages.forwardMessage#33963bf9 peer:InputPeer id:int random_id:long = Updates; messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig; messages.requestEncryption#f64daf43 user_id:InputUser random_id:int g_a:bytes = EncryptedChat; messages.acceptEncryption#3dbc0415 peer:InputEncryptedChat g_b:bytes key_fingerprint:long = EncryptedChat; @@ -1048,8 +1047,9 @@ messages.sendEncryptedService#32d439a4 peer:InputEncryptedChat random_id:long da messages.receivedQueue#55a5bb66 max_qts:int = Vector; messages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool; messages.readMessageContents#36a73f77 id:Vector = messages.AffectedMessages; +messages.getStickers#ae22e045 emoticon:string hash:string = messages.Stickers; messages.getAllStickers#1c9618b1 hash:int = messages.AllStickers; -messages.getWebPagePreview#25223e24 message:string = MessageMedia; +messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector = MessageMedia; messages.exportChatInvite#7d885289 chat_id:int = ExportedChatInvite; messages.checkChatInvite#3eadb1bb hash:string = ChatInvite; messages.importChatInvite#6c50051c hash:string = Updates; @@ -1199,4 +1199,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector = Vector; -// LAYER 74 +// LAYER 75 From 74bffd2ae3e79c18bed379f0c3183801dfc98a1e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 2 Mar 2018 21:32:38 +0100 Subject: [PATCH 252/361] Support multiple captions when sending albums --- telethon/telegram_client.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 24615ace..5323d13a 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1200,7 +1200,8 @@ class TelegramClient(TelegramBareClient): if all(utils.is_image(x) for x in file): return self._send_album( entity, file, caption=caption, - progress_callback=progress_callback, reply_to=reply_to + progress_callback=progress_callback, reply_to=reply_to, + parse_mode=parse_mode ) # Not all are images, so send all the files one by one return [ @@ -1350,10 +1351,13 @@ class TelegramClient(TelegramBareClient): # we need to produce right now to send albums (uploadMedia), and # cache only makes a difference for documents where the user may # want the attributes used on them to change. - # TODO Support a different captions for each file entity = self.get_input_entity(entity) - caption = caption or '' - caption, msg_entities = self._parse_message_text(caption, parse_mode) + if not utils.is_list_like(caption): + caption = (caption,) + captions = [ + self._parse_message_text(caption or '', parse_mode) + for caption in reversed(caption) # Pop from the end (so reverse) + ] reply_to = self._get_message_id(reply_to) # Need to upload the media first, but only if they're not cached yet @@ -1367,11 +1371,13 @@ class TelegramClient(TelegramBareClient): )).photo) self.session.cache_file(fh.md5, fh.size, input_photo) fh = input_photo - media.append(InputSingleMedia( - InputMediaPhoto(fh), - message=caption, - entities=msg_entities - )) + + if captions: + caption, msg_entities = captions.pop() + else: + caption, msg_entities = '', None + media.append(InputSingleMedia(InputMediaPhoto(fh), message=caption, + entities=msg_entities)) # Now we can construct the multi-media request result = self(SendMultiMediaRequest( From 9bf5cb7ed8d15021bd73e5051a7dd01e7af8a33b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 3 Mar 2018 12:28:18 +0200 Subject: [PATCH 253/361] Add new sessions docs --- readthedocs/extra/advanced-usage/sessions.rst | 96 +++++++++++++++---- 1 file changed, 78 insertions(+), 18 deletions(-) diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index fca7828e..ad824837 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -25,29 +25,89 @@ file, so that you can quickly access them by username or phone number. If you're not going to work with updates, or don't need to cache the ``access_hash`` associated with the entities' ID, you can disable this -by setting ``client.session.save_entities = False``, or pass it as a -parameter to the ``TelegramClient``. +by setting ``client.session.save_entities = False``. -If you don't want to save the files as a database, you can also create -your custom ``Session`` subclass and override the ``.save()`` and ``.load()`` -methods. For example, you could save it on a database: +Custom Session Storage +---------------------- + +If you don't want to use the default SQLite session storage, you can also use +one of the other implementations or implement your own storage. + +To use a custom session storage, simply pass the custom session instance to +``TelegramClient`` instead of the session name. + +Currently, there are three implementations of the abstract ``Session`` class: +* MemorySession. Stores session data in Python variables. +* SQLiteSession, the default. Stores sessions in their own SQLite databases. +* AlchemySession. Stores all sessions in a single database via SQLAlchemy. + +Using AlchemySession +~~~~~~~~~~~~~~~~~~~~ +The AlchemySession implementation can store multiple Sessions in the same +database, but to do this, each session instance needs to have access to the +same models and database session. + +To get started, you need to create an ``AlchemySessionContainer`` which will +contain that shared data. The simplest way to use ``AlchemySessionContainer`` +is to simply pass it the database URL: .. code-block:: python - class DatabaseSession(Session): - def save(): - # serialize relevant data to the database + container = AlchemySessionContainer('mysql://user:pass@localhost/telethon') - def load(): - # load relevant data to the database +If you already have SQLAlchemy set up for your own project, you can also pass +the engine separately: + + .. code-block:: python + + my_sqlalchemy_engine = sqlalchemy.create_engine('...') + container = AlchemySessionContainer(engine=my_sqlalchemy_engine) + +By default, the session container will manage table creation/schema updates/etc +automatically. If you want to manage everything yourself, you can pass your +SQLAlchemy Session and ``declarative_base`` instances and set ``manage_tables`` +to ``False``: + + .. code-block:: python + + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy import orm + import sqlalchemy + + ... + + session_factory = orm.sessionmaker(bind=my_sqlalchemy_engine) + session = session_factory() + my_base = declarative_base() + + ... + + container = AlchemySessionContainer(session=session, table_base=my_base, manage_tables=False) + +You always need to provide either ``engine`` or ``session`` to the container. +If you set ``manage_tables=False`` and provide a ``session``, ``engine`` is not +needed. In any other case, ``engine`` is always required. + +After you have your ``AlchemySessionContainer`` instance created, you can +create new sessions by calling ``new_session``: + + .. code-block:: python + + session = container.new_session('some session id') + client = TelegramClient(session) + +where ``some session id`` is an unique identifier for the session. + +Creating your own storage +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The easiest way to create your own implementation is to use MemorySession as +the base and check out how ``SQLiteSession`` or ``AlchemySession`` work. You +can find the relevant Python files under the ``sessions`` directory. -You should read the ````session.py```` source file to know what "relevant -data" you need to keep track of. - - -Sessions and Heroku -------------------- +SQLite Sessions and Heroku +-------------------------- You probably have a newer version of SQLite installed (>= 3.8.2). Heroku uses SQLite 3.7.9 which does not support ``WITHOUT ROWID``. So, if you generated @@ -59,8 +119,8 @@ session file on your Heroku dyno itself. The most complicated is creating a custom buildpack to install SQLite >= 3.8.2. -Generating a Session File on a Heroku Dyno -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Generating a SQLite Session File on a Heroku Dyno +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. note:: Due to Heroku's ephemeral filesystem all dynamically generated From 1e420f7f9136246ea43bed19bdafa597a98564c7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 12:13:42 +0100 Subject: [PATCH 254/361] Document the new abstract session better --- readthedocs/extra/advanced-usage/sessions.rst | 18 +-- telethon/sessions/abstract.py | 109 +++++++++++++++++- 2 files changed, 116 insertions(+), 11 deletions(-) diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index ad824837..66fa0560 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -37,13 +37,13 @@ To use a custom session storage, simply pass the custom session instance to ``TelegramClient`` instead of the session name. Currently, there are three implementations of the abstract ``Session`` class: -* MemorySession. Stores session data in Python variables. -* SQLiteSession, the default. Stores sessions in their own SQLite databases. -* AlchemySession. Stores all sessions in a single database via SQLAlchemy. +* ``MemorySession``. Stores session data in Python variables. +* ``SQLiteSession``, (default). Stores sessions in their own SQLite databases. +* ``AlchemySession``. Stores all sessions in a single database via SQLAlchemy. Using AlchemySession ~~~~~~~~~~~~~~~~~~~~ -The AlchemySession implementation can store multiple Sessions in the same +The ``AlchemySession`` implementation can store multiple Sessions in the same database, but to do this, each session instance needs to have access to the same models and database session. @@ -82,7 +82,9 @@ to ``False``: ... - container = AlchemySessionContainer(session=session, table_base=my_base, manage_tables=False) + container = AlchemySessionContainer( + session=session, table_base=my_base, manage_tables=False + ) You always need to provide either ``engine`` or ``session`` to the container. If you set ``manage_tables=False`` and provide a ``session``, ``engine`` is not @@ -101,9 +103,9 @@ where ``some session id`` is an unique identifier for the session. Creating your own storage ~~~~~~~~~~~~~~~~~~~~~~~~~ -The easiest way to create your own implementation is to use MemorySession as -the base and check out how ``SQLiteSession`` or ``AlchemySession`` work. You -can find the relevant Python files under the ``sessions`` directory. +The easiest way to create your own implementation is to use ``MemorySession`` +as the base and check out how ``SQLiteSession`` or ``AlchemySession`` work. +You can find the relevant Python files under the ``sessions`` directory. SQLite Sessions and Heroku diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index 647a87c1..6f48ae99 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -6,6 +6,7 @@ import os class Session(ABC): def __init__(self): + # Session IDs can be random on every connection self.id = struct.unpack('q', os.urandom(8))[0] self._sequence = 0 @@ -16,6 +17,9 @@ class Session(ABC): self._flood_sleep_threshold = 60 def clone(self, to_instance=None): + """ + Creates a clone of this session file. + """ cloned = to_instance or self.__class__() cloned._report_errors = self.report_errors cloned._flood_sleep_threshold = self.flood_sleep_threshold @@ -23,104 +27,195 @@ class Session(ABC): @abstractmethod def set_dc(self, dc_id, server_address, port): + """ + Sets the information of the data center address and port that + the library should connect to, as well as the data center ID, + which is currently unused. + """ raise NotImplementedError @property @abstractmethod def server_address(self): + """ + Returns the server address where the library should connect to. + """ raise NotImplementedError @property @abstractmethod def port(self): + """ + Returns the port to which the library should connect to. + """ raise NotImplementedError @property @abstractmethod def auth_key(self): + """ + Returns an ``AuthKey`` instance associated with the saved + data center, or ``None`` if a new one should be generated. + """ raise NotImplementedError @auth_key.setter @abstractmethod def auth_key(self, value): + """ + Sets the ``AuthKey`` to be used for the saved data center. + """ raise NotImplementedError @abstractmethod def close(self): - raise NotImplementedError + """ + Called on client disconnection. Should be used to + free any used resources. Can be left empty if none. + """ @abstractmethod def save(self): + """ + Called whenever important properties change. It should + make persist the relevant session information to disk. + """ raise NotImplementedError @abstractmethod def delete(self): + """ + Called upon client.log_out(). Should delete the stored + information from disk since it's not valid anymore. + """ raise NotImplementedError @classmethod @abstractmethod def list_sessions(cls): + """ + Lists available sessions. Not used by the library itself. + """ raise NotImplementedError @abstractmethod def process_entities(self, tlo): + """ + Processes the input ``TLObject`` or ``list`` and saves + whatever information is relevant (e.g., ID or access hash). + """ raise NotImplementedError @abstractmethod def get_input_entity(self, key): + """ + Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``). + The library uses this method whenever an ``InputPeer`` is needed + to suit several purposes (e.g. user only provided its ID or wishes + to use a cached username to avoid extra RPC). + """ raise NotImplementedError @abstractmethod def cache_file(self, md5_digest, file_size, instance): + """ + Caches the given file information persistently, so that it + doesn't need to be re-uploaded in case the file is used again. + + The ``instance`` will be either an ``InputPhoto`` or ``InputDocument``, + both with an ``.id`` and ``.access_hash`` attributes. + """ raise NotImplementedError @abstractmethod def get_file(self, md5_digest, file_size, cls): + """ + Returns an instance of ``cls`` if the ``md5_digest`` and ``file_size`` + match an existing saved record. The class will either be an + ``InputPhoto`` or ``InputDocument``, both with two parameters + ``id`` and ``access_hash`` in that order. + """ raise NotImplementedError @property def salt(self): + """ + Returns the current salt used when encrypting messages. + """ return self._salt @salt.setter def salt(self, value): + """ + Updates the salt (integer) used when encrypting messages. + """ self._salt = value @property def report_errors(self): + """ + Whether RPC errors should be reported + to https://rpc.pwrtelegram.xyz or not. + """ return self._report_errors @report_errors.setter def report_errors(self, value): + """ + Sets the boolean value that indicates whether RPC errors + should be reported to https://rpc.pwrtelegram.xyz or not. + """ self._report_errors = value @property def time_offset(self): + """ + Time offset (in seconds) to be used + in case the local time is incorrect. + """ return self._time_offset @time_offset.setter def time_offset(self, value): + """ + Updates the integer time offset in seconds. + """ self._time_offset = value @property def flood_sleep_threshold(self): + """ + Threshold below which the library should automatically sleep + whenever a FloodWaitError occurs to prevent it from raising. + """ return self._flood_sleep_threshold @flood_sleep_threshold.setter def flood_sleep_threshold(self, value): + """ + Sets the new time threshold (integer, float or timedelta). + """ self._flood_sleep_threshold = value @property def sequence(self): + """ + Current sequence number needed to generate messages. + """ return self._sequence @sequence.setter def sequence(self, value): + """ + Updates the sequence number (integer) value. + """ self._sequence = value def get_new_msg_id(self): - """Generates a new unique message ID based on the current - time (in ms) since epoch""" + """ + Generates a new unique message ID based on the current + time (in ms) since epoch, applying a known time offset. + """ now = time.time() + self._time_offset nanoseconds = int((now - int(now)) * 1e+9) new_msg_id = (int(now) << 32) | (nanoseconds << 2) @@ -133,12 +228,20 @@ class Session(ABC): return new_msg_id def update_time_offset(self, correct_msg_id): + """ + Updates the time offset to the correct + one given a known valid message ID. + """ now = int(time.time()) correct = correct_msg_id >> 32 self._time_offset = correct - now self._last_msg_id = 0 def generate_sequence(self, content_related): + """ + Generates the next sequence number depending on whether + it should be for a content-related query or not. + """ if content_related: result = self._sequence * 2 + 1 self._sequence += 1 From a9c83250a1e35c77ae852a069539b6a853a738ed Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 12:51:35 +0100 Subject: [PATCH 255/361] Small clean-up of the session classes --- telethon/sessions/abstract.py | 3 +-- telethon/sessions/memory.py | 43 ++++++++++++++++++----------------- telethon/sessions/sqlite.py | 9 +++----- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index 6f48ae99..75324077 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -91,12 +91,11 @@ class Session(ABC): raise NotImplementedError @classmethod - @abstractmethod def list_sessions(cls): """ Lists available sessions. Not used by the library itself. """ - raise NotImplementedError + return [] @abstractmethod def process_entities(self, tlo): diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 7ab31b21..df08bf22 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -67,13 +67,6 @@ class MemorySession(Session): def delete(self): pass - @classmethod - def list_sessions(cls): - raise NotImplementedError - - def _entity_values_to_row(self, id, hash, username, phone, name): - return id, hash, username, phone, name - def _entity_to_row(self, e): if not isinstance(e, TLObject): return @@ -103,7 +96,7 @@ class MemorySession(Session): username = username.lower() phone = getattr(e, 'phone', None) name = utils.get_display_name(e) or None - return self._entity_values_to_row(marked_id, p_hash, username, phone, name) + return marked_id, p_hash, username, phone, name def _entities_to_rows(self, tlo): if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): @@ -129,24 +122,32 @@ class MemorySession(Session): self._entities += set(self._entities_to_rows(tlo)) def get_entity_rows_by_phone(self, phone): - rows = [(id, hash) for id, hash, _, found_phone, _ - in self._entities if found_phone == phone] - return rows[0] if rows else None + try: + return next((id, hash) for id, hash, _, found_phone, _ + in self._entities if found_phone == phone) + except StopIteration: + pass def get_entity_rows_by_username(self, username): - rows = [(id, hash) for id, hash, found_username, _, _ - in self._entities if found_username == username] - return rows[0] if rows else None + try: + return next((id, hash) for id, hash, found_username, _, _ + in self._entities if found_username == username) + except StopIteration: + pass def get_entity_rows_by_name(self, name): - rows = [(id, hash) for id, hash, _, _, found_name - in self._entities if found_name == name] - return rows[0] if rows else None + try: + return next((id, hash) for id, hash, _, _, found_name + in self._entities if found_name == name) + except StopIteration: + pass def get_entity_rows_by_id(self, id): - rows = [(id, hash) for found_id, hash, _, _, _ - in self._entities if found_id == id] - return rows[0] if rows else None + try: + return next((id, hash) for found_id, hash, _, _, _ + in self._entities if found_id == id) + except StopIteration: + pass def get_input_entity(self, key): try: @@ -199,6 +200,6 @@ class MemorySession(Session): def get_file(self, md5_digest, file_size, cls): key = (md5_digest, file_size, _SentFileType.from_type(cls)) try: - return self._files[key] + return cls(self._files[key]) except KeyError: return None diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 0423d88a..c764cd21 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -276,18 +276,15 @@ class SQLiteSession(MemorySession): def get_entity_rows_by_username(self, username): return self._fetchone_entity( - 'select id, hash from entities where username=?', - (username,)) + 'select id, hash from entities where username=?', (username,)) def get_entity_rows_by_name(self, name): return self._fetchone_entity( - 'select id, hash from entities where name=?', - (name,)) + 'select id, hash from entities where name=?', (name,)) def get_entity_rows_by_id(self, id): return self._fetchone_entity( - 'select id, hash from entities where id=?', - (id,)) + 'select id, hash from entities where id=?', (id,)) # File processing From 67a782a6d7626828ad7c76876bcf677ea7702e0a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 12:22:02 +0100 Subject: [PATCH 256/361] Fix wrong peer type being used when not found in the session --- telethon/telegram_client.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5323d13a..61539ffa 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2178,17 +2178,16 @@ class TelegramClient(TelegramBareClient): return InputPeerSelf() return utils.get_input_peer(self._get_entity_from_string(peer)) - is_peer = False if isinstance(peer, int): - peer = PeerUser(peer) - is_peer = True - else: - try: - is_peer = peer.SUBCLASS_OF_ID == 0x2d45687 # crc32(b'Peer') - if not is_peer: - return utils.get_input_peer(peer) - except (AttributeError, TypeError): - pass # Attribute if not TLObject, Type if not "casteable" + peer, kind = utils.resolve_id(peer) + peer = kind(peer) + + try: + is_peer = peer.SUBCLASS_OF_ID == 0x2d45687 # crc32(b'Peer') + if not is_peer: + return utils.get_input_peer(peer) + except (AttributeError, TypeError): + is_peer = False if not is_peer: raise TypeError( From 6060b3430cd601a34e143c400b46cb4e28b7612c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 14:02:48 +0100 Subject: [PATCH 257/361] Fix sqlite session clone causing integrity error Triggered on migrations, the dc id would be None. --- telethon/sessions/sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index c764cd21..1170131e 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -198,7 +198,7 @@ class SQLiteSession(MemorySession): # multiple DCs. Probably done differently. c.execute('delete from sessions') c.execute('insert or replace into sessions values (?,?,?,?)', ( - self._dc_id, + self._dc_id or 0, self._server_address, self._port, self._auth_key.key if self._auth_key else b'' From 0e0e7f1c9e9ddc5e41c5378247bc53b90689f252 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 14:59:30 +0100 Subject: [PATCH 258/361] Avoid unnecessary "or 0" in the session classes --- telethon/sessions/memory.py | 4 ++-- telethon/sessions/sqlalchemy.py | 3 +-- telethon/sessions/sqlite.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index df08bf22..e73e7688 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -29,7 +29,7 @@ class MemorySession(Session): def __init__(self): super().__init__() - self._dc_id = None + self._dc_id = 0 self._server_address = None self._port = None self._auth_key = None @@ -38,7 +38,7 @@ class MemorySession(Session): self._entities = set() def set_dc(self, dc_id, server_address, port): - self._dc_id = dc_id + self._dc_id = dc_id or 0 self._server_address = server_address self._port = port diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py index 933f44c2..d4e72f16 100644 --- a/telethon/sessions/sqlalchemy.py +++ b/telethon/sessions/sqlalchemy.py @@ -129,8 +129,7 @@ class AlchemySession(MemorySession): self._auth_key = AuthKey(data=session.auth_key) def clone(self, to_instance=None): - cloned = to_instance or self.__class__(self.container, self.session_id) - return super().clone(cloned) + return super().clone(MemorySession()) def set_dc(self, dc_id, server_address, port): super().set_dc(dc_id, server_address, port) diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 1170131e..c764cd21 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -198,7 +198,7 @@ class SQLiteSession(MemorySession): # multiple DCs. Probably done differently. c.execute('delete from sessions') c.execute('insert or replace into sessions values (?,?,?,?)', ( - self._dc_id or 0, + self._dc_id, self._server_address, self._port, self._auth_key.key if self._auth_key else b'' From 57f50889b08639b2469355fc42c2e22578ed1a82 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 15:07:49 +0100 Subject: [PATCH 259/361] Fix non-aggressive get_participants and inverted condition --- telethon/telegram_client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 61539ffa..fbb57d63 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1090,7 +1090,10 @@ class TelegramClient(TelegramBareClient): if requests[0].offset > limit: break - results = self(*requests) + if len(requests) == 1: + results = (self(requests[0]),) + else: + results = self(*requests) for i in reversed(range(len(requests))): participants = results[i] if not participants.users: @@ -1101,9 +1104,9 @@ class TelegramClient(TelegramBareClient): if len(all_participants) < limit: all_participants[user.id] = user if limit < float('inf'): - values = all_participants.values() - else: values = itertools.islice(all_participants.values(), limit) + else: + values = all_participants.values() users = UserList(values) users.total = total From 81f8b7f76e9516fdc4ec6cf39f52a8d8273c146d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 16:48:57 +0100 Subject: [PATCH 260/361] Add back _entity_values_to_row --- telethon/sessions/memory.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index e73e7688..939f986d 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -67,6 +67,12 @@ class MemorySession(Session): def delete(self): pass + def _entity_values_to_row(self, id, hash, username, phone, name): + # While this is a simple implementation it might be overrode by, + # other classes so they don't need to implement the plural form + # of the method. Don't remove. + return id, hash, username, phone, name + def _entity_to_row(self, e): if not isinstance(e, TLObject): return @@ -96,7 +102,9 @@ class MemorySession(Session): username = username.lower() phone = getattr(e, 'phone', None) name = utils.get_display_name(e) or None - return marked_id, p_hash, username, phone, name + return self._entity_values_to_row( + marked_id, p_hash, username, phone, name + ) def _entities_to_rows(self, tlo): if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): From 96a1f580a4994825ef3fb80d5ee63908ede6ac33 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 17:01:06 +0100 Subject: [PATCH 261/361] Remove useless if condition on the memory session --- telethon/sessions/memory.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 939f986d..42af7ad9 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -116,8 +116,6 @@ class MemorySession(Session): entities.extend(tlo.chats) if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): entities.extend(tlo.users) - if not entities: - return rows = [] # Rows to add (id, hash, username, phone, name) for e in entities: From 0f72aa8f94f5b834af5c934a671449286ce5e2e1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 17:08:49 +0100 Subject: [PATCH 262/361] Fix set union --- telethon/sessions/memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 42af7ad9..4d7e6778 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -125,7 +125,7 @@ class MemorySession(Session): return rows def process_entities(self, tlo): - self._entities += set(self._entities_to_rows(tlo)) + self._entities |= set(self._entities_to_rows(tlo)) def get_entity_rows_by_phone(self, phone): try: From 854c42b7ef25763bb5f14290e32c714164d0ace4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:12:05 +0100 Subject: [PATCH 263/361] Add a file= parameter to client.send_message() --- .../extra/basic/working-with-updates.rst | 4 ++++ telethon/telegram_client.py | 21 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 652f6000..3c57b792 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -99,6 +99,10 @@ done! The event that will be passed always is of type ``XYZ.Event`` (for instance, ``NewMessage.Event``), except for the ``Raw`` event which just passes the ``Update`` object. +Note that ``.reply()`` and ``.respond()`` are just wrappers around the +``client.send_message()`` method which supports the ``file=`` parameter. +This means you can reply with a photo if you do ``client.reply(file=photo)``. + You can put the same event on many handlers, and even different events on the same handler. You can also have a handler work on only specific chats, for example: diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index fbb57d63..a86b257e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -667,8 +667,8 @@ class TelegramClient(TelegramBareClient): return message, msg_entities - def send_message(self, entity, message, reply_to=None, parse_mode='md', - link_preview=True): + def send_message(self, entity, message='', reply_to=None, parse_mode='md', + link_preview=True, file=None, force_document=False): """ Sends the given message to the specified entity (user/chat/channel). @@ -692,9 +692,26 @@ class TelegramClient(TelegramBareClient): link_preview (:obj:`bool`, optional): Should the link preview be shown? + file (:obj:`file`, optional): + Sends a message with a file attached (e.g. a photo, + video, audio or document). The ``message`` may be empty. + + force_document (:obj:`bool`, optional): + Whether to send the given file as a document or not. + Returns: the sent message """ + if file is not None: + return self.send_file( + entity, file, caption=message, reply_to=reply_to, + parse_mode=parse_mode, force_document=force_document + ) + elif not message: + raise ValueError( + 'The message cannot be empty unless a file is provided' + ) + entity = self.get_input_entity(entity) if isinstance(message, Message): if (message.media From c40a3ca77ca8e844203a45d9a51cd21cc6e12a5d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:23:14 +0100 Subject: [PATCH 264/361] Split MessageChanged into MessageEdited and MessageDeleted --- telethon/events/__init__.py | 57 ++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 5966a120..233fc94a 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -829,21 +829,32 @@ class UserUpdate(_EventBuilder): return self.chat -class MessageChanged(_EventBuilder): +class MessageEdited(NewMessage): """ - Represents a message changed (edited or deleted). + Event fired when a message has been edited. """ def build(self, update): if isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): - event = MessageChanged.Event(edit_msg=update.message) - elif isinstance(update, types.UpdateDeleteMessages): - event = MessageChanged.Event( + event = MessageEdited.Event(update.message) + else: + return + + return self._filter_event(event) + + +class MessageDeleted(_EventBuilder): + """ + Event fired when one or more messages are deleted. + """ + def build(self, update): + if isinstance(update, types.UpdateDeleteMessages): + event = MessageDeleted.Event( deleted_ids=update.messages, peer=None ) elif isinstance(update, types.UpdateDeleteChannelMessages): - event = MessageChanged.Event( + event = MessageDeleted.Event( deleted_ids=update.messages, peer=types.PeerChannel(update.channel_id) ) @@ -852,33 +863,13 @@ class MessageChanged(_EventBuilder): return self._filter_event(event) - class Event(NewMessage.Event): - """ - Represents the event of an user status update (last seen, joined). - - Please note that the ``message`` member will be ``None`` if the - action was a deletion and not an edit. - - Members: - edited (:obj:`bool`): - ``True`` if the message was edited. - - deleted (:obj:`bool`): - ``True`` if the message IDs were deleted. - - deleted_ids (:obj:`List[int]`): - A list containing the IDs of the messages that were deleted. - """ - def __init__(self, edit_msg=None, deleted_ids=None, peer=None): - if edit_msg is None: - msg = types.Message((deleted_ids or [0])[0], peer, None, '') - else: - msg = edit_msg - super().__init__(msg) - - self.edited = bool(edit_msg) - self.deleted = bool(deleted_ids) - self.deleted_ids = deleted_ids or [] + class Event(_EventCommon): + def __init__(self, deleted_ids, peer): + super().__init__( + types.Message((deleted_ids or [0])[0], peer, None, '') + ) + self.deleted_id = None if not deleted_ids else deleted_ids[0] + self.deleted_ids = self.deleted_ids class StopPropagation(Exception): From 1c8bf4471308ddd9df7f003523f4123287b61df1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:31:06 +0100 Subject: [PATCH 265/361] Add input user versions to events.ChatAction --- telethon/events/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 233fc94a..821930a6 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -607,6 +607,7 @@ class ChatAction(_EventBuilder): self.created = bool(created) self._user_peers = users if isinstance(users, list) else [users] self._users = None + self._input_users = None self.new_title = new_title @property @@ -665,6 +666,16 @@ class ChatAction(_EventBuilder): except (StopIteration, TypeError): return None + @property + def input_user(self): + """ + Input version of the self.user property. + """ + try: + return next(self.input_users) + except (StopIteration, TypeError): + return None + @property def users(self): """ @@ -681,6 +692,22 @@ class ChatAction(_EventBuilder): return self._users + @property + def input_users(self): + """ + Input version of the self.users property. + """ + if self._input_users is None and self._user_peers: + self._input_users = [] + for peer in self._user_peers: + try: + self._input_users.append(self._client.get_input_entity( + peer + )) + except (TypeError, ValueError): + pass + return self._input_users + class UserUpdate(_EventBuilder): """ From 458d220af9643321442baa605aa07a1a34403b68 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:41:27 +0100 Subject: [PATCH 266/361] Fix users not being set for some events.ChatAction and properties --- telethon/events/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 821930a6..fe9fc821 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -525,15 +525,19 @@ class ChatAction(_EventBuilder): elif isinstance(action, types.MessageActionChannelCreate): event = ChatAction.Event(msg.to_id, created=True, + users=msg.from_id, new_title=action.title) elif isinstance(action, types.MessageActionChatEditTitle): event = ChatAction.Event(msg.to_id, + users=msg.from_id, new_title=action.title) elif isinstance(action, types.MessageActionChatEditPhoto): event = ChatAction.Event(msg.to_id, + users=msg.from_id, new_photo=action.photo) elif isinstance(action, types.MessageActionChatDeletePhoto): event = ChatAction.Event(msg.to_id, + users=msg.from_id, new_photo=True) else: return @@ -661,20 +665,16 @@ class ChatAction(_EventBuilder): Might be ``None`` if the information can't be retrieved or there is no user taking part. """ - try: - return next(self.users) - except (StopIteration, TypeError): - return None + if self.users: + return self._users[0] @property def input_user(self): """ Input version of the self.user property. """ - try: - return next(self.input_users) - except (StopIteration, TypeError): - return None + if self.input_users: + return self._input_users[0] @property def users(self): From 393f505dc8eba464d392187a487449818ead1e88 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:51:35 +0100 Subject: [PATCH 267/361] Put more emphasis on common mistakes when getting entities by IDs --- .../extra/advanced-usage/accessing-the-full-api.rst | 9 +++++++++ readthedocs/extra/basic/entities.rst | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index 7276aa43..edbe821d 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -112,6 +112,15 @@ as you wish. Remember to use the right types! To sum up: )) +This can further be simplified to: + + .. code-block:: python + + result = client(SendMessageRequest('username', 'Hello there!')) + # Or even + result = client(SendMessageRequest(PeerChannel(id), 'Hello there!')) + + .. note:: Note that some requests have a "hash" parameter. This is **not** diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index b68a74d7..84be3250 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -37,12 +37,24 @@ you're able to just do this: # Using Peer/InputPeer (note that the API may return these) # users, chats and channels may all have the same ID, so it's # necessary to wrap (at least) chat and channels inside Peer. + # + # NOTICE how the IDs *must* be wrapped inside a Peer() so the + # library knows their type. from telethon.tl.types import PeerUser, PeerChat, PeerChannel my_user = client.get_entity(PeerUser(some_id)) my_chat = client.get_entity(PeerChat(some_id)) my_channel = client.get_entity(PeerChannel(some_id)) +.. warning:: + + As it has been mentioned already, getting the entity of a channel + through e.g. ``client.get_entity(channel id)`` will **not** work. + You would use ``client.get_entity(types.PeerChannel(channel id))``. + Remember that supergroups are channels and normal groups are chats. + This is a common mistake! + + All methods in the :ref:`telegram-client` call ``.get_input_entity()`` prior to sending the requst to save you from the hassle of doing so manually. That way, convenience calls such as ``client.send_message('lonami', 'hi!')`` From 4de811b8cb6d7e3dddef0bf32ea2066061d68656 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Mar 2018 23:55:35 +0100 Subject: [PATCH 268/361] Expose the client on events as a public property --- telethon/events/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index fe9fc821..942fc342 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -140,6 +140,10 @@ class _EventCommon(abc.ABC): ) return self._input_chat + @property + def client(self): + return self._client + @property def chat(self): """ From 363e751f4896e4e29669723af84a8a175e8ee6ad Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 00:23:13 +0100 Subject: [PATCH 269/361] Fix UserList not being considered a list --- telethon/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telethon/utils.py b/telethon/utils.py index faf69649..a9311521 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -6,6 +6,7 @@ import math import mimetypes import re import types +from collections import UserList from mimetypes import add_type, guess_extension from .tl.types import ( @@ -342,7 +343,8 @@ def is_list_like(obj): enough. Things like open() are also iterable (and probably many other things), so just support the commonly known list-like objects. """ - return isinstance(obj, (list, tuple, set, dict, types.GeneratorType)) + return isinstance(obj, (list, tuple, set, dict, + UserList, types.GeneratorType)) def parse_phone(phone): From 3a13f5f02faea49b72a0917cad8042365fd0d0fa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 00:27:21 +0100 Subject: [PATCH 270/361] Implement a forward_messages convenience method --- telethon/telegram_client.py | 56 ++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a86b257e..f79721a6 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -56,7 +56,8 @@ from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, - UploadMediaRequest, EditMessageRequest, GetFullChatRequest + UploadMediaRequest, EditMessageRequest, GetFullChatRequest, + ForwardMessagesRequest ) from .tl.functions import channels @@ -756,6 +757,59 @@ class TelegramClient(TelegramBareClient): return self._get_response_message(request, result) + def forward_messages(self, entity, messages, from_peer=None): + """ + Forwards the given message(s) to the specified entity. + + Args: + entity (:obj:`entity`): + To which entity the message(s) will be forwarded. + + messages (:obj:`list` | :obj:`int` | :obj:`Message`): + The message(s) to forward, or their integer IDs. + + from_peer (:obj:`entity`): + If the given messages are integer IDs and not instances + of the ``Message`` class, this *must* be specified in + order for the forward to work. + + Returns: + The forwarded messages. + """ + if not utils.is_list_like(messages): + messages = (messages,) + + if not from_peer: + try: + # On private chats (to_id = PeerUser), if the message is + # not outgoing, we actually need to use "from_id" to get + # the conversation on which the message was sent. + from_peer = next( + m.from_id if not m.out and isinstance(m.to_id, PeerUser) + else m.to_id for m in messages if isinstance(m, Message) + ) + except StopIteration: + raise ValueError( + 'from_chat must be given if integer IDs are used' + ) + + req = ForwardMessagesRequest( + from_peer=from_peer, + id=[m if isinstance(m, int) else m.id for m in messages], + to_peer=entity + ) + result = self(req) + random_to_id = {} + id_to_message = {} + for update in result.updates: + if isinstance(update, UpdateMessageID): + random_to_id[update.random_id] = update.id + elif isinstance(update, UpdateNewMessage): + id_to_message[update.message.id] = update.message + + return [id_to_message[random_to_id[rnd]] for rnd in req.random_id] + + def edit_message(self, entity, message_id, message=None, parse_mode='md', link_preview=True): """ From 82c034dc5610ccfec8bdb8fa384641dd2cbb8daa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 00:32:26 +0100 Subject: [PATCH 271/361] Add forward_to on events.NewMessage --- telethon/events/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 942fc342..5c0f2d07 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -320,9 +320,17 @@ class NewMessage(_EventBuilder): Replies to the message (as a reply). This is a shorthand for ``client.send_message(event.chat, ..., reply_to=event.message.id)``. """ - return self._client.send_message(self.input_chat, - reply_to=self.message.id, - *args, **kwargs) + kwargs['reply_to'] = self.message.id + return self._client.send_message(self.input_chat, *args, **kwargs) + + def forward_to(self, *args, **kwargs): + """ + Forwards the message. This is a shorthand for + ``client.forward_messages(entity, event.message, event.chat)``. + """ + kwargs['messages'] = [self.message.id] + kwargs['from_peer'] = self.input_chat + return self._client.forward_messages(*args, **kwargs) def edit(self, *args, **kwargs): """ From e8a21dc3b9b92ffe970f7752ef7cd8fbfca2fb72 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 11:23:18 +0100 Subject: [PATCH 272/361] Fix telethon_generator/ package not being excluded from PyPi --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0e052d31..143ca0cb 100755 --- a/setup.py +++ b/setup.py @@ -147,7 +147,11 @@ def main(): keywords='telegram api chat client library messaging mtproto', packages=find_packages(exclude=[ 'telethon_generator', 'telethon_tests', 'run_tests.py', - 'try_telethon.py' + 'try_telethon.py', + 'telethon_generator/parser/__init__.py', + 'telethon_generator/parser/source_builder.py', + 'telethon_generator/parser/tl_object.py', + 'telethon_generator/parser/tl_parser.py', ]), install_requires=['pyaes', 'rsa'], extras_require={ From fe627d197003ca3774cfaf76b85ca1a4866cb630 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Mar 2018 12:03:09 +0100 Subject: [PATCH 273/361] Update to v0.18 --- readthedocs/extra/changelog.rst | 65 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index ad027361..e8876d5c 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,71 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Sessions overhaul (v0.18) +========================= + +*Published at 2018/03/04* + ++-----------------------+ +| Scheme layer used: 75 | ++-----------------------+ + +The ``Session``'s have been revisited thanks to the work of **@tulir** and +they now use an `ABC `__ so you +can easily implement your own! + +The default will still be a ``SQLiteSession``, but you might want to use +the new ``AlchemySessionContainer`` if you need. Refer to the section of +the documentation on :ref:`sessions` for more. + +Breaking changes +~~~~~~~~~~~~~~~~ + +- ``events.MessageChanged`` doesn't exist anymore. Use the new + ``events.MessageEdited`` and ``events.MessageDeleted`` instead. + +Additions +~~~~~~~~~ + +- The mentioned addition of new session types. +- You can omit the event type on ``client.add_event_handler`` to use ``Raw``. +- You can ``raise StopPropagation`` of events if you added several of them. +- ``.get_participants()`` can now get up to 90,000 members from groups with + 100,000 if when ``aggressive=True``, "bypassing" Telegram's limit. +- You now can access ``NewMessage.Event.pattern_match``. +- Multiple captions are now supported when sending albums. +- ``client.send_message()`` has an optional ``file=`` parameter, so + you can do ``events.reply(file='/path/to/photo.jpg')`` and similar. +- Added ``.input_`` versions to ``events.ChatAction``. +- You can now access the public ``.client`` property on ``events``. +- New ``client.forward_messages``, with its own wrapper on ``events``, + called ``event.forward_to(...)``. + + +Bug fixes +~~~~~~~~~ + +- Silly bug regarding ``client.get_me(input_peer=True)``. +- ``client.send_voice_note()`` was missing some parameters. +- ``client.send_file()`` plays better with streams now. +- Incoming messages from bots weren't working with whitelists. +- Markdown's URL regex was not accepting newlines. +- Better attempt at joining background update threads. +- Use the right peer type when a marked integer ID is provided. + + +Internal changes +~~~~~~~~~~~~~~~~ + +- Resolving ``events.Raw`` is now a no-op. +- Logging calls in the ``TcpClient`` to spot errors. +- ``events`` resolution is postponed until you are successfully connected, + so you can attach them before starting the client. +- When an entity is not found, it is searched in *all* dialogs. This might + not always be desirable but it's more comfortable for legitimate uses. +- Some non-persisting properties from the ``Session`` have been moved out. + + Further easing library usage (v0.17.4) ====================================== diff --git a/telethon/version.py b/telethon/version.py index 8cc14b33..90e8bfe4 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.17.4' +__version__ = '0.18' From 4f880dcd569247e8ef9f0d2a4d59a7dd7f2f45f3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 6 Mar 2018 12:09:37 +0100 Subject: [PATCH 274/361] Replace BLOB with LargeBinary in sqlalchemy.py (closes #670) --- telethon/sessions/sqlalchemy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py index d4e72f16..dc9040a1 100644 --- a/telethon/sessions/sqlalchemy.py +++ b/telethon/sessions/sqlalchemy.py @@ -1,6 +1,6 @@ try: from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy import Column, String, Integer, BLOB, orm + from sqlalchemy import Column, String, Integer, LargeBinary, orm import sqlalchemy as sql except ImportError: sql = None @@ -59,7 +59,7 @@ class AlchemySessionContainer: dc_id = Column(Integer, primary_key=True) server_address = Column(String) port = Column(Integer) - auth_key = Column(BLOB) + auth_key = Column(LargeBinary) class Entity(Base): query = db.query_property() @@ -77,7 +77,7 @@ class AlchemySessionContainer: __tablename__ = '{prefix}sent_files'.format(prefix=prefix) session_id = Column(String, primary_key=True) - md5_digest = Column(BLOB, primary_key=True) + md5_digest = Column(LargeBinary, primary_key=True) file_size = Column(Integer, primary_key=True) type = Column(Integer, primary_key=True) id = Column(Integer) From 7201482ebdb002e47b27aba65f7b67da8093e4a1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 6 Mar 2018 12:24:37 +0100 Subject: [PATCH 275/361] Support limit=0 on .get_participants to fetch count only --- telethon/telegram_client.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f79721a6..f514e7c8 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -809,7 +809,6 @@ class TelegramClient(TelegramBareClient): return [id_to_message[random_to_id[rnd]] for rnd in req.random_id] - def edit_message(self, entity, message_id, message=None, parse_mode='md', link_preview=True): """ @@ -963,7 +962,8 @@ class TelegramClient(TelegramBareClient): # No messages, but we still need to know the total message count result = self(GetHistoryRequest( peer=entity, limit=1, - offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0 + offset_date=None, offset_id=0, max_id=0, min_id=0, + add_offset=0, hash=0 )) return getattr(result, 'count', len(result.messages)), [], [] @@ -1131,6 +1131,10 @@ class TelegramClient(TelegramBareClient): total = self(GetFullChannelRequest( entity )).full_chat.participants_count + if limit == 0: + users = UserList() + users.total = total + return users all_participants = {} if total > 10000 and aggressive: @@ -1188,7 +1192,7 @@ class TelegramClient(TelegramBareClient): users = UserList(users) users.total = len(users) else: - users = UserList([entity]) + users = UserList(None if limit == 0 else [entity]) users.total = 1 return users From e3adec5ea9fe554bffd2d63bd759c68a0bd19e4d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Mar 2018 09:09:05 +0100 Subject: [PATCH 276/361] Fix caption being None This would later be an empty string with some modifications that were removed upon upgrading to layer 75, which changed where the captions are used and their naming. --- 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 f514e7c8..dbe026c6 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1200,7 +1200,7 @@ class TelegramClient(TelegramBareClient): # region Uploading files - def send_file(self, entity, file, caption=None, + def send_file(self, entity, file, caption='', force_document=False, progress_callback=None, reply_to=None, attributes=None, @@ -1420,7 +1420,7 @@ class TelegramClient(TelegramBareClient): kwargs['is_voice_note'] = True return self.send_file(*args, **kwargs) - def _send_album(self, entity, files, caption=None, + def _send_album(self, entity, files, caption='', progress_callback=None, reply_to=None, parse_mode='md'): """Specialized version of .send_file for albums""" From dd6802e032a1f3c162bd9e3404b00ad3c2f5818f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Mar 2018 11:45:37 +0100 Subject: [PATCH 277/361] Support PhotoSize in .download_media (#669) This simplifies downloading thumbnails (and any other PhotoSize). --- telethon/telegram_client.py | 42 +++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index dbe026c6..e1914eb5 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -82,8 +82,8 @@ from .tl.types import ( InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, InputMessageEntityMentionName, DocumentAttributeVideo, UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, - MessageMediaWebPage, ChannelParticipantsSearch -) + MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize, + PhotoSizeEmpty) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -1720,7 +1720,8 @@ class TelegramClient(TelegramBareClient): date = datetime.now() media = message - if isinstance(media, (MessageMediaPhoto, Photo)): + if isinstance(media, (MessageMediaPhoto, Photo, + PhotoSize, PhotoCachedSize)): return self._download_photo( media, file, date, progress_callback ) @@ -1738,24 +1739,39 @@ class TelegramClient(TelegramBareClient): # Determine the photo and its largest size if isinstance(photo, MessageMediaPhoto): photo = photo.photo - if not isinstance(photo, Photo): + if isinstance(photo, Photo): + for size in reversed(photo.sizes): + if not isinstance(size, PhotoSizeEmpty): + photo = size + break + else: + return + if not isinstance(photo, (PhotoSize, PhotoCachedSize)): return - largest_size = photo.sizes[-1] - file_size = largest_size.size - largest_size = largest_size.location - file = self._get_proper_filename(file, 'photo', '.jpg', date=date) + if isinstance(photo, PhotoCachedSize): + # No need to download anything, simply write the bytes + if isinstance(file, str): + helpers.ensure_parent_dir_exists(file) + f = open(file, 'wb') + else: + f = file + try: + f.write(photo.bytes) + finally: + if isinstance(file, str): + f.close() + return file - # Download the media with the largest size input file location self.download_file( InputFileLocation( - volume_id=largest_size.volume_id, - local_id=largest_size.local_id, - secret=largest_size.secret + volume_id=photo.location.volume_id, + local_id=photo.location.local_id, + secret=photo.location.secret ), file, - file_size=file_size, + file_size=photo.size, progress_callback=progress_callback ) return file From d0bdb7ea3f381a536ea3226ac28c485a3ae3fd46 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Mar 2018 11:13:55 +0100 Subject: [PATCH 278/361] Lower message severity when retrying invoke the first time --- telethon/telegram_bare_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index bf33a7dc..7164bb17 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -450,9 +450,10 @@ class TelegramBareClient: if result is not None: return result - __log__.warning('Invoking %s failed %d times, ' - 'reconnecting and retrying', - [str(x) for x in requests], retry + 1) + log = __log__.info if retry == 0 else __log__.warning + log('Invoking %s failed %d times, connecting again and retrying', + [str(x) for x in requests], retry + 1) + sleep(1) # The ReadThread has priority when attempting reconnection, # since this thread is constantly running while __call__ is From fca4904d0f7fbad067ca40a3f64e78fe98947e35 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Mar 2018 11:30:03 +0100 Subject: [PATCH 279/361] Add more logging calls when confirming a request --- telethon/network/mtproto_sender.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index cbcdc76d..532a8da7 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -69,6 +69,7 @@ class MtProtoSender: def disconnect(self): """Disconnects from the server.""" + __log__.info('Disconnecting MtProtoSender...') self.connection.close() self._need_confirmation.clear() self._clear_all_pending() @@ -244,6 +245,7 @@ class MtProtoSender: if r: r.result = True # Telegram won't send this value r.confirm_received.set() + __log__.debug('Confirmed %s through ack', type(r).__name__) return True @@ -252,6 +254,7 @@ class MtProtoSender: if r: r.result = obj r.confirm_received.set() + __log__.debug('Confirmed %s through salt', type(r).__name__) # If the object isn't any of the above, then it should be an Update. self.session.process_entities(obj) @@ -308,6 +311,7 @@ class MtProtoSender: """ for r in self._pending_receive.values(): r.request.confirm_received.set() + __log__.info('Abruptly confirming %s', type(r).__name__) self._pending_receive.clear() def _resend_request(self, msg_id): @@ -337,6 +341,7 @@ class MtProtoSender: if request: request.result = pong request.confirm_received.set() + __log__.debug('Confirmed %s through pong', type(request).__name__) return True @@ -490,6 +495,9 @@ class MtProtoSender: if request: request.rpc_error = error request.confirm_received.set() + + __log__.debug('Confirmed %s through error %s', + type(request).__name__, error) # else TODO Where should this error be reported? # Read may be async. Can an error not-belong to a request? return True # All contents were read okay @@ -505,6 +513,10 @@ class MtProtoSender: self.session.process_entities(request.result) request.confirm_received.set() + __log__.debug( + 'Confirmed %s through normal result %s', + type(request).__name__, type(request.result).__name__ + ) return True # If it's really a result for RPC from previous connection From dc99d119c321613a4c0b226dfe23175f84e86deb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Mar 2018 17:29:05 +0100 Subject: [PATCH 280/361] Fix events.MessageDeleted always failing due to extra "self." --- telethon/events/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 5c0f2d07..f12af888 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -908,7 +908,7 @@ class MessageDeleted(_EventBuilder): types.Message((deleted_ids or [0])[0], peer, None, '') ) self.deleted_id = None if not deleted_ids else deleted_ids[0] - self.deleted_ids = self.deleted_ids + self.deleted_ids = deleted_ids class StopPropagation(Exception): From 801018fa9b84b2b8e67a512b1a4cc748d2771564 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Mar 2018 17:51:59 +0100 Subject: [PATCH 281/361] Add respond, reply and delete methods to events.ChatAction Also introduces the new .action_message member. --- telethon/events/__init__.py | 63 ++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index f12af888..d84ed043 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -518,37 +518,37 @@ class ChatAction(_EventBuilder): msg = update.message action = update.message.action if isinstance(action, types.MessageActionChatJoinedByLink): - event = ChatAction.Event(msg.to_id, + event = ChatAction.Event(msg, added_by=True, users=msg.from_id) elif isinstance(action, types.MessageActionChatAddUser): - event = ChatAction.Event(msg.to_id, + event = ChatAction.Event(msg, added_by=msg.from_id or True, users=action.users) elif isinstance(action, types.MessageActionChatDeleteUser): - event = ChatAction.Event(msg.to_id, + event = ChatAction.Event(msg, kicked_by=msg.from_id or True, users=action.user_id) elif isinstance(action, types.MessageActionChatCreate): - event = ChatAction.Event(msg.to_id, + event = ChatAction.Event(msg, users=action.users, created=True, new_title=action.title) elif isinstance(action, types.MessageActionChannelCreate): - event = ChatAction.Event(msg.to_id, + event = ChatAction.Event(msg, created=True, users=msg.from_id, new_title=action.title) elif isinstance(action, types.MessageActionChatEditTitle): - event = ChatAction.Event(msg.to_id, + event = ChatAction.Event(msg, users=msg.from_id, new_title=action.title) elif isinstance(action, types.MessageActionChatEditPhoto): - event = ChatAction.Event(msg.to_id, + event = ChatAction.Event(msg, users=msg.from_id, new_photo=action.photo) elif isinstance(action, types.MessageActionChatDeletePhoto): - event = ChatAction.Event(msg.to_id, + event = ChatAction.Event(msg, users=msg.from_id, new_photo=True) else: @@ -591,10 +591,17 @@ class ChatAction(_EventBuilder): new_title (:obj:`bool`, optional): The new title string for the chat, if applicable. """ - def __init__(self, chat_peer, new_pin=None, new_photo=None, + def __init__(self, where, new_pin=None, new_photo=None, added_by=None, kicked_by=None, created=None, - users=None, new_title=None): - super().__init__(chat_peer=chat_peer, msg_id=new_pin) + users=None, new_title=None, action_message=None): + if isinstance(where, types.MessageService): + self.action_message = where + where = where.to_id + else: + self.action_message = None + + super().__init__(chat_peer=where, msg_id=new_pin) + self.action_message = action_message self.new_pin = isinstance(new_pin, int) self._pinned_message = new_pin @@ -626,6 +633,40 @@ class ChatAction(_EventBuilder): self._input_users = None self.new_title = new_title + def respond(self, *args, **kwargs): + """ + Responds to the chat action message (not as a reply). + Shorthand for ``client.send_message(event.chat, ...)``. + """ + return self._client.send_message(self.input_chat, *args, **kwargs) + + def reply(self, *args, **kwargs): + """ + Replies to the chat action message (as a reply). Shorthand for + ``client.send_message(event.chat, ..., reply_to=event.message.id)``. + + Has the same effect as ``.respond()`` if there is no message. + """ + if not self.action_message: + return self.respond(*args, **kwargs) + + kwargs['reply_to'] = self.action_message.id + return self._client.send_message(self.input_chat, *args, **kwargs) + + def delete(self, *args, **kwargs): + """ + Deletes the chat action message. You're responsible for checking + whether you have the permission to do so, or to except the error + otherwise. This is a shorthand for + ``client.delete_messages(event.chat, event.message, ...)``. + + Does nothing if no message action triggered this event. + """ + if self.action_message: + return self._client.delete_messages(self.input_chat, + [self.action_message], + *args, **kwargs) + @property def pinned_message(self): """ From d3d190f36ecfb92b3ec0b48794070b1f0ea8cbe1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Mar 2018 17:57:54 +0100 Subject: [PATCH 282/361] Fix-up previous commit overriding .action_message with None --- telethon/events/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d84ed043..ec866dd5 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -593,7 +593,7 @@ class ChatAction(_EventBuilder): """ def __init__(self, where, new_pin=None, new_photo=None, added_by=None, kicked_by=None, created=None, - users=None, new_title=None, action_message=None): + users=None, new_title=None): if isinstance(where, types.MessageService): self.action_message = where where = where.to_id @@ -601,7 +601,6 @@ class ChatAction(_EventBuilder): self.action_message = None super().__init__(chat_peer=where, msg_id=new_pin) - self.action_message = action_message self.new_pin = isinstance(new_pin, int) self._pinned_message = new_pin From ce0dee63b1609c1a848f837e47977aaac6945038 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 10:05:40 +0100 Subject: [PATCH 283/361] Support getting any entity by just their positive ID --- telethon/sessions/memory.py | 25 ++++++++++++++------ telethon/sessions/sqlalchemy.py | 23 +++++++++++++----- telethon/sessions/sqlite.py | 19 +++++++++++---- telethon/telegram_client.py | 42 +++++++++++++++++++-------------- 4 files changed, 74 insertions(+), 35 deletions(-) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 4d7e6778..43ddde4b 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -1,9 +1,8 @@ from enum import Enum -from .. import utils from .abstract import Session +from .. import utils from ..tl import TLObject - from ..tl.types import ( PeerUser, PeerChat, PeerChannel, InputPeerUser, InputPeerChat, InputPeerChannel, @@ -148,10 +147,19 @@ class MemorySession(Session): except StopIteration: pass - def get_entity_rows_by_id(self, id): + def get_entity_rows_by_id(self, id, exact=True): try: - return next((id, hash) for found_id, hash, _, _, _ - in self._entities if found_id == id) + if exact: + return next((id, hash) for found_id, hash, _, _, _ + in self._entities if found_id == id) + else: + ids = ( + utils.get_peer_id(PeerUser(id)), + utils.get_peer_id(PeerChat(id)), + utils.get_peer_id(PeerChannel(id)) + ) + return next((id, hash) for found_id, hash, _, _, _ + in self._entities if found_id in ids) except StopIteration: pass @@ -167,6 +175,9 @@ class MemorySession(Session): # Not a TLObject or can't be cast into InputPeer if isinstance(key, TLObject): key = utils.get_peer_id(key) + exact = True + else: + exact = False result = None if isinstance(key, str): @@ -178,8 +189,8 @@ class MemorySession(Session): if username: result = self.get_entity_rows_by_username(username) - if isinstance(key, int): - result = self.get_entity_rows_by_id(key) + elif isinstance(key, int): + result = self.get_entity_rows_by_id(key, exact) if not result and isinstance(key, str): result = self.get_entity_rows_by_name(key) diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py index dc9040a1..ceaa0847 100644 --- a/telethon/sessions/sqlalchemy.py +++ b/telethon/sessions/sqlalchemy.py @@ -4,12 +4,13 @@ try: import sqlalchemy as sql except ImportError: sql = None - pass - -from ..crypto import AuthKey -from ..tl.types import InputPhoto, InputDocument from .memory import MemorySession, _SentFileType +from .. import utils +from ..crypto import AuthKey +from ..tl.types import ( + InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel +) LATEST_VERSION = 1 @@ -201,8 +202,18 @@ class AlchemySession(MemorySession): self.Entity.name == key).one_or_none() return row.id, row.hash if row else None - def get_entity_rows_by_id(self, key): - row = self._db_query(self.Entity, self.Entity.id == key).one_or_none() + def get_entity_rows_by_id(self, key, exact=True): + if exact: + query = self._db_query(self.Entity, self.Entity.id == key) + else: + ids = ( + utils.get_peer_id(PeerUser(key)), + utils.get_peer_id(PeerChat(key)), + utils.get_peer_id(PeerChannel(key)) + ) + query = self._db_query(self.Entity, self.Entity.id in ids) + + row = query.one_or_none() return row.id, row.hash if row else None def get_file(self, md5_digest, file_size, cls): diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index c764cd21..e9a4a723 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -6,9 +6,10 @@ from os.path import isfile as file_exists from threading import Lock, RLock from .memory import MemorySession, _SentFileType +from .. import utils from ..crypto import AuthKey from ..tl.types import ( - InputPhoto, InputDocument + InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel ) EXTENSION = '.session' @@ -282,9 +283,19 @@ class SQLiteSession(MemorySession): return self._fetchone_entity( 'select id, hash from entities where name=?', (name,)) - def get_entity_rows_by_id(self, id): - return self._fetchone_entity( - 'select id, hash from entities where id=?', (id,)) + def get_entity_rows_by_id(self, id, exact=True): + if exact: + return self._fetchone_entity( + 'select id, hash from entities where id=?', (id,)) + else: + ids = ( + utils.get_peer_id(PeerUser(id)), + utils.get_peer_id(PeerChat(id)), + utils.get_peer_id(PeerChannel(id)) + ) + return self._fetchone_entity( + 'select id, hash from entities where id in (?,?,?)', ids + ) # File processing diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e1914eb5..87f57945 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2272,23 +2272,21 @@ class TelegramClient(TelegramBareClient): return InputPeerSelf() return utils.get_input_peer(self._get_entity_from_string(peer)) - if isinstance(peer, int): - peer, kind = utils.resolve_id(peer) - peer = kind(peer) + if not isinstance(peer, int): + try: + if peer.SUBCLASS_OF_ID != 0x2d45687: # crc32(b'Peer') + return utils.get_input_peer(peer) + except (AttributeError, TypeError): + peer = None - try: - is_peer = peer.SUBCLASS_OF_ID == 0x2d45687 # crc32(b'Peer') - if not is_peer: - return utils.get_input_peer(peer) - except (AttributeError, TypeError): - is_peer = False - - if not is_peer: + if not peer: raise TypeError( 'Cannot turn "{}" into an input entity.'.format(peer) ) - # Not found, look in the dialogs with the hope to find it. + # Add the mark to the peers if the user passed a Peer (not an int) + # Look in the dialogs with the hope to find it. + mark = not isinstance(peer, int) target_id = utils.get_peer_id(peer) req = GetDialogsRequest( offset_date=None, @@ -2299,12 +2297,20 @@ class TelegramClient(TelegramBareClient): while True: result = self(req) entities = {} - for x in itertools.chain(result.users, result.chats): - x_id = utils.get_peer_id(x) - if x_id == target_id: - return utils.get_input_peer(x) - else: - entities[x_id] = x + if mark: + for x in itertools.chain(result.users, result.chats): + x_id = utils.get_peer_id(x) + if x_id == target_id: + return utils.get_input_peer(x) + else: + entities[x_id] = x + else: + for x in itertools.chain(result.users, result.chats): + if x.id == target_id: + return utils.get_input_peer(x) + else: + entities[utils.get_peer_id(x)] = x + if len(result.dialogs) < req.limit: break From 0f34a9b3336117aadfc6309d997e7e4a12871d5a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 10:08:26 +0100 Subject: [PATCH 284/361] Fix .get_input_entity error message always showing None --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 87f57945..937540da 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2272,6 +2272,7 @@ class TelegramClient(TelegramBareClient): return InputPeerSelf() return utils.get_input_peer(self._get_entity_from_string(peer)) + original_peer = peer if not isinstance(peer, int): try: if peer.SUBCLASS_OF_ID != 0x2d45687: # crc32(b'Peer') @@ -2281,7 +2282,7 @@ class TelegramClient(TelegramBareClient): if not peer: raise TypeError( - 'Cannot turn "{}" into an input entity.'.format(peer) + 'Cannot turn "{}" into an input entity.'.format(original_peer) ) # Add the mark to the peers if the user passed a Peer (not an int) From 3a3ae75b4615e1116b88e73b3e121c0943e3be4c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 10:12:43 +0100 Subject: [PATCH 285/361] Fix-up bot API style IDs not working on .get_input_entity --- telethon/sessions/memory.py | 2 +- telethon/telegram_client.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 43ddde4b..e5223cac 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -177,7 +177,7 @@ class MemorySession(Session): key = utils.get_peer_id(key) exact = True else: - exact = False + exact = not isinstance(key, int) or key < 0 result = None if isinstance(key, str): diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 937540da..e35b996c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2285,9 +2285,10 @@ class TelegramClient(TelegramBareClient): 'Cannot turn "{}" into an input entity.'.format(original_peer) ) - # Add the mark to the peers if the user passed a Peer (not an int) + # Add the mark to the peers if the user passed a Peer (not an int), + # or said ID is negative. If it's negative it's been marked already. # Look in the dialogs with the hope to find it. - mark = not isinstance(peer, int) + mark = not isinstance(peer, int) or peer < 0 target_id = utils.get_peer_id(peer) req = GetDialogsRequest( offset_date=None, From 841aed13da0ec4baed362725ab3c5a89e5bf2db7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 10:16:46 +0100 Subject: [PATCH 286/361] Fix tuple/ternary operator fail on SQLAlchemy session (#671) --- telethon/sessions/sqlalchemy.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py index ceaa0847..a24df485 100644 --- a/telethon/sessions/sqlalchemy.py +++ b/telethon/sessions/sqlalchemy.py @@ -159,8 +159,9 @@ class AlchemySession(MemorySession): self.db.merge(new) def _db_query(self, dbclass, *args): - return dbclass.query.filter(dbclass.session_id == self.session_id, - *args) + return dbclass.query.filter( + dbclass.session_id == self.session_id, *args + ) def save(self): self.container.save() @@ -190,17 +191,17 @@ class AlchemySession(MemorySession): def get_entity_rows_by_phone(self, key): row = self._db_query(self.Entity, self.Entity.phone == key).one_or_none() - return row.id, row.hash if row else None + return (row.id, row.hash) if row else None def get_entity_rows_by_username(self, key): row = self._db_query(self.Entity, self.Entity.username == key).one_or_none() - return row.id, row.hash if row else None + return (row.id, row.hash) if row else None def get_entity_rows_by_name(self, key): row = self._db_query(self.Entity, self.Entity.name == key).one_or_none() - return row.id, row.hash if row else None + return (row.id, row.hash) if row else None def get_entity_rows_by_id(self, key, exact=True): if exact: @@ -214,7 +215,7 @@ class AlchemySession(MemorySession): query = self._db_query(self.Entity, self.Entity.id in ids) row = query.one_or_none() - return row.id, row.hash if row else None + return (row.id, row.hash) if row else None def get_file(self, md5_digest, file_size, cls): row = self._db_query(self.SentFile, @@ -222,7 +223,7 @@ class AlchemySession(MemorySession): self.SentFile.file_size == file_size, self.SentFile.type == _SentFileType.from_type( cls).value).one_or_none() - return row.id, row.hash if row else None + return (row.id, row.hash) if row else None def cache_file(self, md5_digest, file_size, instance): if not isinstance(instance, (InputDocument, InputPhoto)): From 09f0f86f1e6fe6ef8df5527f09dcf1a71496175d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 10:30:49 +0100 Subject: [PATCH 287/361] Add convenience NewMessage attrs to get media of specific types --- telethon/events/__init__.py | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index ec866dd5..8bf7b51c 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -482,6 +482,52 @@ class NewMessage(_EventBuilder): if isinstance(doc, types.Document): return doc + def _document_by_attribute(self, kind, condition=None): + """ + Helper method to return the document only if it has an attribute + that's an instance of the given kind, and passes the condition. + """ + doc = self.document + if doc: + for attr in doc.attributes: + if isinstance(attr, kind): + if not condition or condition(doc): + return doc + + @property + def audio(self): + """ + If the message media is a document with an Audio attribute, + this returns the (:obj:`Document`) object. + """ + return self._document_by_attribute(types.DocumentAttributeAudio, + lambda attr: not attr.voice) + + @property + def voice(self): + """ + If the message media is a document with a Voice attribute, + this returns the (:obj:`Document`) object. + """ + return self._document_by_attribute(types.DocumentAttributeAudio, + lambda attr: attr.voice) + + @property + def video(self): + """ + If the message media is a document with a Video attribute, + this returns the (:obj:`Document`) object. + """ + return self._document_by_attribute(types.DocumentAttributeVideo) + + @property + def sticker(self): + """ + If the message media is a document with a Sticker attribute, + this returns the (:obj:`Document`) object. + """ + return self._document_by_attribute(types.DocumentAttributeSticker) + @property def out(self): """ From 567386655338e66a0c4018cdca2537c76810b533 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 11:44:13 +0100 Subject: [PATCH 288/361] Create client.iter_ versions for all client.get_ methods While doing so, the client.iter_drafts method has been simplified as it made some unnecessary calls. client.get_message_history has been shortened to client.get_messages, and fixes a bug where the limit being zero made it return a tuple. client.iter_messages also uses a local dictionary for entities so it should become less big in memory (and possibly faster). client.get_participants would fail with user entities, returning only their input version. --- telethon/telegram_client.py | 259 ++++++++++++++++++++++-------------- 1 file changed, 158 insertions(+), 101 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e35b996c..d62b15fe 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -90,6 +90,12 @@ from .extensions import markdown, html __log__ = logging.getLogger(__name__) +class _Box: + """Helper class to pass parameters by reference""" + def __init__(self, x=None): + self.x = x + + class TelegramClient(TelegramBareClient): """ Initializes the Telegram client with the specified API ID and Hash. @@ -508,10 +514,11 @@ class TelegramClient(TelegramBareClient): # region Dialogs ("chats") requests - def get_dialogs(self, limit=10, offset_date=None, offset_id=0, - offset_peer=InputPeerEmpty()): + def iter_dialogs(self, limit=None, offset_date=None, offset_id=0, + offset_peer=InputPeerEmpty(), _total_box=None): """ - Gets N "dialogs" (open "chats" or conversations with other people). + Returns an iterator over the dialogs, yielding 'limit' at most. + Dialogs are the open "chats" or conversations with other people. Args: limit (:obj:`int` | :obj:`None`): @@ -530,11 +537,16 @@ class TelegramClient(TelegramBareClient): offset_peer (:obj:`InputPeer`, optional): The peer to be used as an offset. - Returns: - A list dialogs, with an additional .total attribute on the list. + _total_box (:obj:`_Box`, optional): + A _Box instance to pass the total parameter by reference. + + Yields: + Instances of ``telethon.tl.custom.Dialog``. """ limit = float('inf') if limit is None else int(limit) if limit == 0: + if not _total_box: + return # Special case, get a single dialog and determine count dialogs = self(GetDialogsRequest( offset_date=offset_date, @@ -542,14 +554,12 @@ class TelegramClient(TelegramBareClient): offset_peer=offset_peer, limit=1 )) - result = UserList() - result.total = getattr(dialogs, 'count', len(dialogs.dialogs)) - return result + _total_box.x = getattr(dialogs, 'count', len(dialogs.dialogs)) + return - total_count = 0 - dialogs = OrderedDict() # Use peer id as identifier to avoid dupes - while len(dialogs) < limit: - real_limit = min(limit - len(dialogs), 100) + seen = set() + while len(seen) < limit: + real_limit = min(limit - len(seen), 100) r = self(GetDialogsRequest( offset_date=offset_date, offset_id=offset_id, @@ -557,14 +567,17 @@ class TelegramClient(TelegramBareClient): limit=real_limit )) - total_count = getattr(r, 'count', len(r.dialogs)) + if _total_box: + _total_box.x = getattr(r, 'count', len(r.dialogs)) messages = {m.id: m for m in r.messages} entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} for d in r.dialogs: - dialogs[utils.get_peer_id(d.peer)] = \ - Dialog(self, d, entities, messages) + peer_id = utils.get_peer_id(d.peer) + if peer_id not in seen: + seen.add(peer_id) + yield Dialog(self, d, entities, messages) if len(r.dialogs) < real_limit or not isinstance(r, DialogsSlice): # Less than we requested means we reached the end, or @@ -575,26 +588,33 @@ class TelegramClient(TelegramBareClient): offset_peer = entities[utils.get_peer_id(r.dialogs[-1].peer)] offset_id = r.messages[-1].id - dialogs = UserList( - itertools.islice(dialogs.values(), min(limit, len(dialogs))) - ) - dialogs.total = total_count + def get_dialogs(self, *args, **kwargs): + """ + Same as :meth:`iter_dialogs`, but returns a list instead + with an additional .total attribute on the list. + """ + total_box = _Box(0) + kwargs['_total_box'] = total_box + dialogs = UserList(self.iter_dialogs(*args, **kwargs)) + dialogs.total = total_box.x return dialogs - def get_drafts(self): # TODO: Ability to provide a `filter` + def iter_drafts(self): # TODO: Ability to provide a `filter` """ - Gets all open draft messages. + Iterator over all open draft messages. - 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 :meth:`draft.delete()`. + The yielded items are custom ``Draft`` objects that are easier to use. + You can call ``draft.set_message('text')`` to change the message, + or delete it through :meth:`draft.delete()`. """ - 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 + for update in self(GetAllDraftsRequest()).updates: + yield Draft._from_update(self, update) + + def get_drafts(self): + """ + Same as :meth:`iter_drafts`, but returns a list instead. + """ + return list(self.iter_drafts()) @staticmethod def _get_response_message(request, result): @@ -891,11 +911,11 @@ class TelegramClient(TelegramBareClient): else: return self(messages.DeleteMessagesRequest(message_ids, revoke=revoke)) - def get_message_history(self, entity, limit=20, offset_date=None, - offset_id=0, max_id=0, min_id=0, add_offset=0, - batch_size=100, wait_time=None): + def iter_messages(self, entity, limit=20, offset_date=None, + offset_id=0, max_id=0, min_id=0, add_offset=0, + batch_size=100, wait_time=None, _total_box=None): """ - Gets the message history for the specified entity + Iterator over the message history for the specified entity. Args: entity (:obj:`entity`): @@ -939,10 +959,12 @@ class TelegramClient(TelegramBareClient): If left to ``None``, it will default to 1 second only if the limit is higher than 3000. - Returns: - A list of messages with extra attributes: + _total_box (:obj:`_Box`, optional): + A _Box instance to pass the total parameter by reference. + + Yields: + Instances of ``telethon.tl.types.Message`` with extra attributes: - * ``.total`` = (on the list) total amount of messages sent. * ``.sender`` = entity of the sender. * ``.fwd_from.sender`` = if fwd_from, who sent it originally. * ``.fwd_from.channel`` = if fwd_from, original channel. @@ -959,25 +981,26 @@ class TelegramClient(TelegramBareClient): entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) if limit == 0: + if not _total_box: + return # No messages, but we still need to know the total message count result = self(GetHistoryRequest( peer=entity, limit=1, offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0, hash=0 )) - return getattr(result, 'count', len(result.messages)), [], [] + _total_box.x = getattr(result, 'count', len(result.messages)) + return if wait_time is None: wait_time = 1 if limit > 3000 else 0 + have = 0 batch_size = min(max(batch_size, 1), 100) - total_messages = 0 - messages = UserList() - entities = {} - while len(messages) < limit: + while have < limit: # Telegram has a hard limit of 100 - real_limit = min(limit - len(messages), batch_size) - result = self(GetHistoryRequest( + real_limit = min(limit - have, batch_size) + r = self(GetHistoryRequest( peer=entity, limit=real_limit, offset_date=offset_date, @@ -987,48 +1010,63 @@ class TelegramClient(TelegramBareClient): add_offset=add_offset, hash=0 )) - messages.extend( - m for m in result.messages if not isinstance(m, MessageEmpty) - ) - total_messages = getattr(result, 'count', len(result.messages)) + if _total_box: + _total_box.x = getattr(r, 'count', len(r.messages)) - for u in result.users: - entities[utils.get_peer_id(u)] = u - for c in result.chats: - entities[utils.get_peer_id(c)] = c + entities = {utils.get_peer_id(x): x + for x in itertools.chain(r.users, r.chats)} - if len(result.messages) < real_limit: + for message in r.messages: + if isinstance(message, MessageEmpty): + continue + + # Add a few extra attributes to the Message to be friendlier. + # To make messages more friendly, always add message + # to service messages, and action to normal messages. + message.message = getattr(message, 'message', None) + message.action = getattr(message, 'action', None) + message.to = entities[utils.get_peer_id(message.to_id)] + message.sender = ( + None if not message.from_id else + entities[utils.get_peer_id(message.from_id)] + ) + if getattr(message, 'fwd_from', None): + message.fwd_from.sender = ( + None if not message.fwd_from.from_id else + entities[utils.get_peer_id(message.fwd_from.from_id)] + ) + message.fwd_from.channel = ( + None if not message.fwd_from.channel_id else + entities[utils.get_peer_id( + PeerChannel(message.fwd_from.channel_id) + )] + ) + yield message + have += 1 + + if len(r.messages) < real_limit: break - offset_id = result.messages[-1].id - offset_date = result.messages[-1].date + offset_id = r.messages[-1].id + offset_date = r.messages[-1].date time.sleep(wait_time) - # Add a few extra attributes to the Message to make it friendlier. - messages.total = total_messages - for m in messages: - # To make messages more friendly, always add message - # to service messages, and action to normal messages. - m.message = getattr(m, 'message', None) - m.action = getattr(m, 'action', None) - m.sender = (None if not m.from_id else - entities[utils.get_peer_id(m.from_id)]) + def get_messages(self, *args, **kwargs): + """ + Same as :meth:`iter_messages`, but returns a list instead + with an additional .total attribute on the list. + """ + total_box = _Box(0) + kwargs['_total_box'] = total_box + msgs = UserList(self.iter_messages(*args, **kwargs)) + msgs.total = total_box.x + return msgs - if getattr(m, 'fwd_from', None): - m.fwd_from.sender = ( - None if not m.fwd_from.from_id else - entities[utils.get_peer_id(m.fwd_from.from_id)] - ) - m.fwd_from.channel = ( - None if not m.fwd_from.channel_id else - entities[utils.get_peer_id( - PeerChannel(m.fwd_from.channel_id) - )] - ) - - m.to = entities[utils.get_peer_id(m.to_id)] - - return messages + def get_message_history(self, *args, **kwargs): + warnings.warn( + 'get_message_history is deprecated, use get_messages instead' + ) + return self.get_messages(*args, **kwargs) def send_read_acknowledge(self, entity, message=None, max_id=None, clear_mentions=False): @@ -1096,8 +1134,8 @@ class TelegramClient(TelegramBareClient): raise TypeError('Invalid message type: {}'.format(type(message))) - def get_participants(self, entity, limit=None, search='', - aggressive=False): + def iter_participants(self, entity, limit=None, search='', + aggressive=False, _total_box=None): """ Gets the list of participants from the specified entity. @@ -1121,6 +1159,9 @@ class TelegramClient(TelegramBareClient): This has no effect for groups or channels with less than 10,000 members. + _total_box (:obj:`_Box`, optional): + A _Box instance to pass the total parameter by reference. + Returns: A list of participants with an additional .total variable on the list indicating the total amount of members in this group/channel. @@ -1131,12 +1172,13 @@ class TelegramClient(TelegramBareClient): total = self(GetFullChannelRequest( entity )).full_chat.participants_count - if limit == 0: - users = UserList() - users.total = total - return users + if _total_box: + _total_box.x = total - all_participants = {} + if limit == 0: + return + + seen = set() if total > 10000 and aggressive: requests = [GetParticipantsRequest( channel=entity, @@ -1176,25 +1218,40 @@ class TelegramClient(TelegramBareClient): else: requests[i].offset += len(participants.users) for user in participants.users: - if len(all_participants) < limit: - all_participants[user.id] = user - if limit < float('inf'): - values = itertools.islice(all_participants.values(), limit) - else: - values = all_participants.values() + if user.id not in seen: + seen.add(user.id) + yield user + if len(seen) >= limit: + return - users = UserList(values) - users.total = total elif isinstance(entity, InputPeerChat): users = self(GetFullChatRequest(entity.chat_id)).users - if len(users) > limit: - users = users[:limit] - users = UserList(users) - users.total = len(users) + if _total_box: + _total_box.x = len(users) + + have = 0 + for user in users: + have += 1 + if have > limit: + break + else: + yield user else: - users = UserList(None if limit == 0 else [entity]) - users.total = 1 - return users + if _total_box: + _total_box.x = 1 + if limit != 0: + yield self.get_entity(entity) + + def get_participants(self, *args, **kwargs): + """ + Same as :meth:`iter_participants`, but returns a list instead + with an additional .total attribute on the list. + """ + total_box = _Box(0) + kwargs['_total_box'] = total_box + dialogs = UserList(self.iter_participants(*args, **kwargs)) + dialogs.total = total_box.x + return dialogs # endregion From 3d49f740dfe8ea41282a06a85bc49d90f71c841a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 11:48:59 +0100 Subject: [PATCH 289/361] Use the new client.iter_dialogs() in client.get_input_entity() --- telethon/telegram_client.py | 40 ++++++++----------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index d62b15fe..c6ce80e4 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2347,38 +2347,14 @@ class TelegramClient(TelegramBareClient): # Look in the dialogs with the hope to find it. mark = not isinstance(peer, int) or peer < 0 target_id = utils.get_peer_id(peer) - req = GetDialogsRequest( - offset_date=None, - offset_id=0, - offset_peer=InputPeerEmpty(), - limit=100 - ) - while True: - result = self(req) - entities = {} - if mark: - for x in itertools.chain(result.users, result.chats): - x_id = utils.get_peer_id(x) - if x_id == target_id: - return utils.get_input_peer(x) - else: - entities[x_id] = x - else: - for x in itertools.chain(result.users, result.chats): - if x.id == target_id: - return utils.get_input_peer(x) - else: - entities[utils.get_peer_id(x)] = x - - if len(result.dialogs) < req.limit: - break - - req.offset_id = result.messages[-1].id - req.offset_date = result.messages[-1].date - req.offset_peer = entities[utils.get_peer_id( - result.dialogs[-1].peer - )] - time.sleep(1) + if mark: + for dialog in self.iter_dialogs(): + if utils.get_peer_id(dialog.entity) == target_id: + return utils.get_input_peer(dialog.entity) + else: + for dialog in self.iter_dialogs(): + if dialog.entity.id == target_id: + return utils.get_input_peer(dialog.entity) raise TypeError( 'Could not find the input entity corresponding to "{}". ' From 6e6d40be1881b58e47e60ca5e95204b384d65de5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 12:37:06 +0100 Subject: [PATCH 290/361] Implement Draft.send() (closes #673) --- telethon/telegram_client.py | 13 ++++++++++--- telethon/tl/custom/draft.py | 6 ++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c6ce80e4..ef707c99 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -689,7 +689,8 @@ class TelegramClient(TelegramBareClient): return message, msg_entities def send_message(self, entity, message='', reply_to=None, parse_mode='md', - link_preview=True, file=None, force_document=False): + link_preview=True, file=None, force_document=False, + clear_draft=False): """ Sends the given message to the specified entity (user/chat/channel). @@ -720,6 +721,10 @@ class TelegramClient(TelegramBareClient): force_document (:obj:`bool`, optional): Whether to send the given file as a document or not. + clear_draft (:obj:`bool`, optional): + Whether the existing draft should be cleared or not. + Has no effect when sending a file. + Returns: the sent message """ @@ -750,7 +755,8 @@ class TelegramClient(TelegramBareClient): reply_to_msg_id=reply_id, reply_markup=message.reply_markup, entities=message.entities, - no_webpage=not isinstance(message.media, MessageMediaWebPage) + no_webpage=not isinstance(message.media, MessageMediaWebPage), + clear_draft=clear_draft ) message = message.message else: @@ -760,7 +766,8 @@ class TelegramClient(TelegramBareClient): message=message, entities=msg_ent, no_webpage=not link_preview, - reply_to_msg_id=self._get_message_id(reply_to) + reply_to_msg_id=self._get_message_id(reply_to), + clear_draft=clear_draft ) result = self(request) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index 9b800d4c..bea57f49 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -74,6 +74,12 @@ class Draft: return result + def send(self, clear=True): + self._client.send_message(self._peer, self.text, + reply_to=self.reply_to_msg_id, + link_preview=not self.no_webpage, + clear_draft=clear) + def delete(self): """ Deletes this draft From 8cefb22e142e043a763ac2159f919141d972423f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 12:56:20 +0100 Subject: [PATCH 291/361] Add .text and .raw_text properties to the Draft class (#673) --- telethon/tl/custom/draft.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index bea57f49..1c28a007 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -1,5 +1,6 @@ from ..functions.messages import SaveDraftRequest from ..types import UpdateDraftMessage, DraftMessage +from ...extensions import markdown class Draft: @@ -14,11 +15,11 @@ class Draft: if not draft: draft = DraftMessage('', None, None, None, None) - self.text = draft.message + self._text = markdown.unparse(draft.message, draft.entities) + self._raw_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): @@ -38,7 +39,16 @@ class Draft: 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): + @property + def text(self): + return self._text + + @property + def raw_text(self): + return self._raw_text + + def set_message(self, text, no_webpage=None, reply_to_msg_id=None, + parse_mode='md'): """ Changes the draft message on the Telegram servers. The changes are reflected in this object. Changing only individual attributes like for @@ -52,32 +62,34 @@ class Draft: 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 + :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 str parse_mode: The parse mode to be used for the text. + :return bool: ``True`` on success. """ + raw_text, entities = self._client._parse_message_text(text, parse_mode) result = self._client(SaveDraftRequest( peer=self._peer, - message=text, + message=raw_text, no_webpage=no_webpage, reply_to_msg_id=reply_to_msg_id, entities=entities )) if result: - self.text = text + self._text = text + self._raw_text = raw_text self.no_webpage = no_webpage self.reply_to_msg_id = reply_to_msg_id - self.entities = entities return result - def send(self, clear=True): + def send(self, clear=True, parse_mode='md'): self._client.send_message(self._peer, self.text, reply_to=self.reply_to_msg_id, link_preview=not self.no_webpage, + parse_mode=parse_mode, clear_draft=clear) def delete(self): From 9d46bb35c86ae7ff9112283602d9d6f37364db33 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 13:04:01 +0100 Subject: [PATCH 292/361] Rename and reorder some params in Draft for consistency (#673) --- telethon/tl/custom/draft.py | 46 +++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index 1c28a007..8f3aac60 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -18,7 +18,7 @@ class Draft: self._text = markdown.unparse(draft.message, draft.entities) self._raw_text = draft.message self.date = draft.date - self.no_webpage = draft.no_webpage + self.link_preview = not draft.no_webpage self.reply_to_msg_id = draft.reply_to_msg_id @classmethod @@ -47,48 +47,54 @@ class Draft: def raw_text(self): return self._raw_text - def set_message(self, text, no_webpage=None, reply_to_msg_id=None, - parse_mode='md'): + def set_message(self, text=None, reply_to=0, parse_mode='md', + link_preview=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 - ) + reflected in this object. :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. + Preserved if left as None. + + :param int reply_to: Message ID to reply to. + Preserved if left as 0, erased if set to None. + + :param bool link_preview: Whether to attach a web page preview. + Preserved if left as None. + :param str parse_mode: The parse mode to be used for the text. :return bool: ``True`` on success. """ + if text is None: + text = self._text + + if reply_to == 0: + reply_to = self.reply_to_msg_id + + if link_preview is None: + link_preview = self.link_preview + raw_text, entities = self._client._parse_message_text(text, parse_mode) result = self._client(SaveDraftRequest( peer=self._peer, message=raw_text, - no_webpage=no_webpage, - reply_to_msg_id=reply_to_msg_id, + no_webpage=not link_preview, + reply_to_msg_id=reply_to, entities=entities )) if result: self._text = text self._raw_text = raw_text - self.no_webpage = no_webpage - self.reply_to_msg_id = reply_to_msg_id + self.link_preview = link_preview + self.reply_to_msg_id = reply_to return result def send(self, clear=True, parse_mode='md'): self._client.send_message(self._peer, self.text, reply_to=self.reply_to_msg_id, - link_preview=not self.no_webpage, + link_preview=self.link_preview, parse_mode=parse_mode, clear_draft=clear) From cf650e061e14cacb1e55d3a5e330730f99360ebf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 20:18:10 +0100 Subject: [PATCH 293/361] Avoid editing events.NewMessage that are forwards --- telethon/events/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 8bf7b51c..1bd9b8e7 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -340,6 +340,8 @@ class NewMessage(_EventBuilder): Returns ``None`` if the message was incoming, or the edited message otherwise. """ + if self.message.fwd_from: + return None if not self.message.out: if not isinstance(self.message.to_id, types.PeerUser): return None From 2fb42772c606382cbe2b88944bc5c1b22ce1444e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Mar 2018 20:21:56 +0100 Subject: [PATCH 294/361] Add .video_note and .gif convenience properties to NewMessage --- telethon/events/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 1bd9b8e7..76aab2a5 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -522,6 +522,23 @@ class NewMessage(_EventBuilder): """ return self._document_by_attribute(types.DocumentAttributeVideo) + @property + def video_note(self): + """ + If the message media is a document with a Video attribute, + this returns the (:obj:`Document`) object. + """ + return self._document_by_attribute(types.DocumentAttributeVideo, + lambda attr: attr.round_message) + + @property + def gif(self): + """ + If the message media is a document with an Animated attribute, + this returns the (:obj:`Document`) object. + """ + return self._document_by_attribute(types.DocumentAttributeAnimated) + @property def sticker(self): """ From 1ad7712fde65985e5ca6d1c875f47e91bb2d9991 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 10 Mar 2018 11:52:31 +0100 Subject: [PATCH 295/361] Automatically redirect on documentation for exact matches --- docs/res/js/search.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/res/js/search.js b/docs/res/js/search.js index c63672e7..1b33980b 100644 --- a/docs/res/js/search.js +++ b/docs/res/js/search.js @@ -147,6 +147,7 @@ function updateSearch() { } else { exactMatch.style.display = ""; buildList(null, exactList, [destination, destinationu]); + return destinationu[0]; } } else { contentDiv.style.display = ""; @@ -169,4 +170,8 @@ if (query) { searchBox.value = query; } -updateSearch(); +var exactUrl = updateSearch(); +var redirect = getQuery('redirect'); +if (exactUrl && redirect != 'no') { + window.location = exactUrl; +} From e088fc3a4e6835939562b27a7cbe248761b9809c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 10 Mar 2018 12:13:17 +0100 Subject: [PATCH 296/361] Add extra safety checks when getting peer ID --- telethon/utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/telethon/utils.py b/telethon/utils.py index a9311521..8e37714e 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -381,6 +381,17 @@ def parse_username(username): return None, False +def _fix_peer_id(peer_id): + """ + Fixes the peer ID for chats and channels, in case the users + mix marking the ID with the ``Peer()`` constructors. + """ + peer_id = abs(peer_id) + if str(peer_id).startswith('100'): + peer_id = str(peer_id)[3:] + return int(peer_id) + + def get_peer_id(peer): """ Finds the ID of the given peer, and converts it to the "bot api" format @@ -408,6 +419,10 @@ def get_peer_id(peer): if isinstance(peer, (PeerUser, InputPeerUser)): return peer.user_id elif isinstance(peer, (PeerChat, InputPeerChat)): + # Check in case the user mixed things up to avoid blowing up + if not (0 < peer.chat_id <= 0x7fffffff): + peer.chat_id = _fix_peer_id(peer.chat_id) + return -peer.chat_id elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)): if isinstance(peer, ChannelFull): @@ -416,6 +431,15 @@ def get_peer_id(peer): i = peer.id else: i = peer.channel_id + + # Check in case the user mixed things up to avoid blowing up + if not (0 < i <= 0x7fffffff): + i = _fix_peer_id(i) + if isinstance(peer, ChannelFull): + peer.id = i + else: + peer.channel_id = i + # Concat -100 through math tricks, .to_supergroup() on Madeline # IDs will be strictly positive -> log works return -(i + pow(10, math.floor(math.log10(i) + 3))) From 70ef93a62ecf1a5aadb87276e8c2d3c4a48e38db Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 11 Mar 2018 09:38:52 +0100 Subject: [PATCH 297/361] Stop treating image/webp as images as Telegram throws error --- telethon/utils.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index 8e37714e..daf8c875 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -55,9 +55,6 @@ def get_display_name(entity): return '' -# For some reason, .webp (stickers' format) is not registered -add_type('image/webp', '.webp') - def get_extension(media): """Gets the corresponding extension for any Telegram media""" @@ -319,8 +316,10 @@ def get_input_media(media, is_photo=False): def is_image(file): """Returns True if the file extension looks like an image file""" - return (isinstance(file, str) and - (mimetypes.guess_type(file)[0] or '').startswith('image/')) + if not isinstance(file, str): + return False + mime = mimetypes.guess_type(file)[0] or '' + return mime.startswith('image/') and not mime.endswith('/webp') def is_audio(file): From 055aa7fe433f0ac847a6556564748e7a347765f5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 11 Mar 2018 09:43:55 +0100 Subject: [PATCH 298/361] Fix MessageService not handled on .delete_messages (closes #681) --- telethon/telegram_client.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ef707c99..f7e5d1a9 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -8,7 +8,7 @@ import re import sys import time import warnings -from collections import OrderedDict, UserList +from collections import UserList from datetime import datetime, timedelta from io import BytesIO from mimetypes import guess_type @@ -83,7 +83,8 @@ from .tl.types import ( InputMessageEntityMentionName, DocumentAttributeVideo, UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize, - PhotoSizeEmpty) + PhotoSizeEmpty, MessageService +) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -903,10 +904,13 @@ class TelegramClient(TelegramBareClient): Returns: The affected messages. """ + if not utils.is_list_like(message_ids): + message_ids = (message_ids,) - if not isinstance(message_ids, list): - message_ids = [message_ids] - message_ids = [m.id if isinstance(m, Message) else int(m) for m in message_ids] + message_ids = [ + m.id if isinstance(m, (Message, MessageService, MessageEmpty)) + else int(m) for m in message_ids + ] if entity is None: return self(messages.DeleteMessagesRequest(message_ids, revoke=revoke)) From a596f88497921856fbf5b1639a0a9cf925739e80 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 11 Mar 2018 09:48:48 +0100 Subject: [PATCH 299/361] Fix wrong super() args for events.MessageDeleted (fix #675) --- telethon/events/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 76aab2a5..a4ec83b2 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1010,7 +1010,7 @@ class MessageDeleted(_EventBuilder): class Event(_EventCommon): def __init__(self, deleted_ids, peer): super().__init__( - types.Message((deleted_ids or [0])[0], peer, None, '') + chat_peer=peer, msg_id=(deleted_ids or [0])[0] ) self.deleted_id = None if not deleted_ids else deleted_ids[0] self.deleted_ids = deleted_ids From 8b1cc4c8cbf4685d75161d6497788afd9a944bed Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 11 Mar 2018 09:55:31 +0100 Subject: [PATCH 300/361] Better handle pinned dialogs and limit on .get_dialogs() --- telethon/telegram_client.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f7e5d1a9..2c5bbaf2 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -559,14 +559,15 @@ class TelegramClient(TelegramBareClient): return seen = set() + req = GetDialogsRequest( + offset_date=offset_date, + offset_id=offset_id, + offset_peer=offset_peer, + limit=0 + ) while len(seen) < limit: - real_limit = min(limit - len(seen), 100) - r = self(GetDialogsRequest( - offset_date=offset_date, - offset_id=offset_id, - offset_peer=offset_peer, - limit=real_limit - )) + req.limit = min(limit - len(seen), 100) + r = self(req) if _total_box: _total_box.x = getattr(r, 'count', len(r.dialogs)) @@ -574,20 +575,25 @@ class TelegramClient(TelegramBareClient): entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} + # Happens when there are pinned dialogs + if len(r.dialogs) > limit: + r.dialogs = r.dialogs[:limit] + for d in r.dialogs: peer_id = utils.get_peer_id(d.peer) if peer_id not in seen: seen.add(peer_id) yield Dialog(self, d, entities, messages) - if len(r.dialogs) < real_limit or not isinstance(r, DialogsSlice): + if len(r.dialogs) < req.limit or not isinstance(r, DialogsSlice): # Less than we requested means we reached the end, or # we didn't get a DialogsSlice which means we got all. break - offset_date = r.messages[-1].date - offset_peer = entities[utils.get_peer_id(r.dialogs[-1].peer)] - offset_id = r.messages[-1].id + req.offset_date = r.messages[-1].date + req.offset_peer = entities[utils.get_peer_id(r.dialogs[-1].peer)] + req.offset_id = r.messages[-1].id + req.exclude_pinned = True def get_dialogs(self, *args, **kwargs): """ From 657c771fa0134ee9662f86bbf6dbcdec7b2ef222 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 12 Mar 2018 09:52:16 +0100 Subject: [PATCH 301/361] Fix incorrect participant count on some channels --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 2c5bbaf2..68f5e810 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1233,7 +1233,7 @@ class TelegramClient(TelegramBareClient): if not participants.users: requests.pop(i) else: - requests[i].offset += len(participants.users) + requests[i].offset += len(participants.participants) for user in participants.users: if user.id not in seen: seen.add(user.id) From 751461f0f559458e0f8c69692c66e76523e197a9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 12 Mar 2018 10:27:49 +0100 Subject: [PATCH 302/361] Modify iter_participants to also include .participant info --- telethon/telegram_client.py | 38 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 68f5e810..bd83f938 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1154,7 +1154,7 @@ class TelegramClient(TelegramBareClient): def iter_participants(self, entity, limit=None, search='', aggressive=False, _total_box=None): """ - Gets the list of participants from the specified entity. + Iterator over the participants belonging to the specified chat. Args: entity (:obj:`entity`): @@ -1179,9 +1179,11 @@ class TelegramClient(TelegramBareClient): _total_box (:obj:`_Box`, optional): A _Box instance to pass the total parameter by reference. - Returns: - A list of participants with an additional .total variable on the - list indicating the total amount of members in this group/channel. + Yields: + The ``User`` objects returned by ``GetParticipantsRequest`` + with an additional ``.participant`` attribute which is the + matched ``ChannelParticipant`` type for channels/megagroups + or ``ChatParticipants`` for normal chats. """ entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) @@ -1234,30 +1236,38 @@ class TelegramClient(TelegramBareClient): requests.pop(i) else: requests[i].offset += len(participants.participants) - for user in participants.users: - if user.id not in seen: - seen.add(user.id) + users = {user.id: user for user in participants.users} + for participant in participants.participants: + if participant.user_id not in seen: + seen.add(participant.user_id) + user = users[participant.user_id] + user.participant = participant yield user if len(seen) >= limit: return elif isinstance(entity, InputPeerChat): - users = self(GetFullChatRequest(entity.chat_id)).users + full = self(GetFullChatRequest(entity.chat_id)) if _total_box: - _total_box.x = len(users) + _total_box.x = len(full.full_chat.participants.participants) have = 0 - for user in users: + users = {user.id: user for user in full.users} + for participant in full.full_chat.participants.participants: have += 1 if have > limit: break else: + user = users[participant.user_id] + user.participant = participant yield user else: if _total_box: _total_box.x = 1 if limit != 0: - yield self.get_entity(entity) + user = self.get_entity(entity) + user.participant = None + yield user def get_participants(self, *args, **kwargs): """ @@ -1266,9 +1276,9 @@ class TelegramClient(TelegramBareClient): """ total_box = _Box(0) kwargs['_total_box'] = total_box - dialogs = UserList(self.iter_participants(*args, **kwargs)) - dialogs.total = total_box.x - return dialogs + participants = UserList(self.iter_participants(*args, **kwargs)) + participants.total = total_box.x + return participants # endregion From 935de0afbb480b366d650f59217507824b5b2cd4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 12 Mar 2018 11:58:56 +0200 Subject: [PATCH 303/361] Add Python type hints to attributes of TL types (#678) --- requirements.txt | 1 + setup.py | 5 +- telethon_generator/parser/tl_object.py | 23 +++++++- telethon_generator/tl_generator.py | 76 +++++++++++++++++++++++++- 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2b650ec4..45e8c141 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pyaes rsa +typing diff --git a/setup.py b/setup.py index 143ca0cb..05ca9197 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ Extra supported commands are: # To use a consistent encoding from codecs import open -from sys import argv +from sys import argv, version_info import os import re @@ -153,7 +153,8 @@ def main(): 'telethon_generator/parser/tl_object.py', 'telethon_generator/parser/tl_parser.py', ]), - install_requires=['pyaes', 'rsa'], + install_requires=['pyaes', 'rsa', + 'typing' if version_info < (3, 5) else ""], extras_require={ 'cryptg': ['cryptg'], 'sqlalchemy': ['sqlalchemy'] diff --git a/telethon_generator/parser/tl_object.py b/telethon_generator/parser/tl_object.py index 034cb3c3..0e0045d7 100644 --- a/telethon_generator/parser/tl_object.py +++ b/telethon_generator/parser/tl_object.py @@ -254,7 +254,7 @@ class TLArg: self.generic_definition = generic_definition - def type_hint(self): + def doc_type_hint(self): result = { 'int': 'int', 'long': 'int', @@ -272,6 +272,27 @@ class TLArg: return result + def python_type_hint(self): + type = self.type + if '.' in type: + type = type.split('.')[1] + result = { + 'int': 'int', + 'long': 'int', + 'int128': 'int', + 'int256': 'int', + 'string': 'str', + 'date': 'Optional[datetime]', # None date = 0 timestamp + 'bytes': 'bytes', + 'true': 'bool', + }.get(type, "Type{}".format(type)) + if self.is_vector: + result = 'List[{}]'.format(result) + if self.is_flag and type != 'date': + result = 'Optional[{}]'.format(result) + + return result + def __str__(self): # Find the real type representation by updating it as required real_type = self.type diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index ff12acfe..7c1f6237 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -138,6 +138,7 @@ class TLGenerator: builder.writeln( 'from {}.tl.tlobject import TLObject'.format('.' * depth) ) + builder.writeln('from typing import Optional, List, Union, TYPE_CHECKING') # Add the relative imports to the namespaces, # unless we already are in a namespace. @@ -154,13 +155,81 @@ class TLGenerator: # Import struct for the .__bytes__(self) serialization builder.writeln('import struct') + tlobjects.sort(key=lambda x: x.name) + + type_names = set() + type_defs = [] + + # Find all the types in this file and generate type definitions + # based on the types. The type definitions are written to the + # file at the end. + for t in tlobjects: + if not t.is_function: + type_name = t.result + if '.' in type_name: + type_name = type_name[type_name.rindex('.'):] + if type_name in type_names: + continue + type_names.add(type_name) + constructors = type_constructors[type_name] + if not constructors: + pass + elif len(constructors) == 1: + type_defs.append('Type{} = {}'.format( + type_name, constructors[0].class_name())) + else: + type_defs.append('Type{} = Union[{}]'.format( + type_name, ','.join(c.class_name() + for c in constructors))) + + imports = {} + primitives = ('int', 'long', 'int128', 'int256', 'string', + 'date', 'bytes', 'true') + # Find all the types in other files that are used in this file + # and generate the information required to import those types. + for t in tlobjects: + for arg in t.args: + name = arg.type + if not name or name in primitives: + continue + + import_space = '{}.tl.types'.format('.' * depth) + if '.' in name: + namespace = name.split('.')[0] + name = name.split('.')[1] + import_space += '.{}'.format(namespace) + + if name not in type_names: + type_names.add(name) + if name == 'date': + imports['datetime'] = ['datetime'] + continue + elif not import_space in imports: + imports[import_space] = set() + imports[import_space].add('Type{}'.format(name)) + + # Add imports required for type checking. + builder.writeln('if TYPE_CHECKING:') + for namespace, names in imports.items(): + builder.writeln('from {} import {}'.format( + namespace, ', '.join(names))) + else: + builder.writeln('pass') + builder.end_block() + # Generate the class for every TLObject - for t in sorted(tlobjects, key=lambda x: x.name): + for t in tlobjects: TLGenerator._write_source_code( t, builder, depth, type_constructors ) builder.current_indent = 0 + # Write the type definitions generated earlier. + builder.writeln('') + for line in type_defs: + builder.writeln(line) + + @staticmethod def _write_source_code(tlobject, builder, depth, type_constructors): """Writes the source code corresponding to the given TLObject @@ -218,7 +287,7 @@ class TLGenerator: for arg in args: if not arg.flag_indicator: builder.writeln(':param {} {}:'.format( - arg.type_hint(), arg.name + arg.doc_type_hint(), arg.name )) builder.current_indent -= 1 # It will auto-indent (':') @@ -258,7 +327,8 @@ class TLGenerator: for arg in args: if not arg.can_be_inferred: - builder.writeln('self.{0} = {0}'.format(arg.name)) + builder.writeln('self.{0} = {0} # type: {1}'.format( + arg.name, arg.python_type_hint())) continue # Currently the only argument that can be From a13433653674ffb34f8fc74d01ad202dcb6bed76 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 13 Mar 2018 12:43:59 +0100 Subject: [PATCH 304/361] Fix ResolveUsernameRequest may return ChannelForbidden --- telethon/telegram_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index bd83f938..c022cbbb 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2312,7 +2312,8 @@ class TelegramClient(TelegramBareClient): return self.get_me() result = self(ResolveUsernameRequest(username)) for entity in itertools.chain(result.users, result.chats): - if entity.username.lower() == username: + if getattr(entity, 'username', None) or ''\ + .lower() == username: return entity try: # Nobody with this username, maybe it's an exact name/title From fd309f0407b7244cfc5390f8678d4f9ec3f9455d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 13 Mar 2018 13:15:02 +0100 Subject: [PATCH 305/361] Add filter parameter to iter_participants and fix search for chats --- telethon/telegram_client.py | 54 ++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c022cbbb..02bf918e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1152,7 +1152,7 @@ class TelegramClient(TelegramBareClient): raise TypeError('Invalid message type: {}'.format(type(message))) def iter_participants(self, entity, limit=None, search='', - aggressive=False, _total_box=None): + filter=None, aggressive=False, _total_box=None): """ Iterator over the participants belonging to the specified chat. @@ -1166,6 +1166,12 @@ class TelegramClient(TelegramBareClient): search (:obj:`str`, optional): Look for participants with this string in name/username. + filter (:obj:`ChannelParticipantsFilter`, optional): + The filter to be used, if you want e.g. only admins. See + https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html. + Note that you might not have permissions for some filter. + This has no effect for normal chats or users. + aggressive (:obj:`bool`, optional): Aggressively looks for all participants in the chat in order to get more than 10,000 members (a hard limit @@ -1174,7 +1180,7 @@ class TelegramClient(TelegramBareClient): participants on groups with 100,000 members. This has no effect for groups or channels with less than - 10,000 members. + 10,000 members, or if a ``filter`` is given. _total_box (:obj:`_Box`, optional): A _Box instance to pass the total parameter by reference. @@ -1185,7 +1191,21 @@ class TelegramClient(TelegramBareClient): matched ``ChannelParticipant`` type for channels/megagroups or ``ChatParticipants`` for normal chats. """ + if isinstance(filter, type): + filter = filter() + entity = self.get_input_entity(entity) + if search and (filter or not isinstance(entity, InputPeerChannel)): + # We need to 'search' ourselves unless we have a PeerChannel + search = search.lower() + + def filter_entity(ent): + return search in utils.get_display_name(ent).lower() or\ + search in (getattr(ent, 'username', '') or None).lower() + else: + def filter_entity(ent): + return True + limit = float('inf') if limit is None else int(limit) if isinstance(entity, InputPeerChannel): total = self(GetFullChannelRequest( @@ -1198,7 +1218,7 @@ class TelegramClient(TelegramBareClient): return seen = set() - if total > 10000 and aggressive: + if total > 10000 and aggressive and not filter: requests = [GetParticipantsRequest( channel=entity, filter=ChannelParticipantsSearch(search + chr(x)), @@ -1209,7 +1229,7 @@ class TelegramClient(TelegramBareClient): else: requests = [GetParticipantsRequest( channel=entity, - filter=ChannelParticipantsSearch(search), + filter=filter or ChannelParticipantsSearch(search), offset=0, limit=200, hash=0 @@ -1238,15 +1258,19 @@ class TelegramClient(TelegramBareClient): requests[i].offset += len(participants.participants) users = {user.id: user for user in participants.users} for participant in participants.participants: - if participant.user_id not in seen: - seen.add(participant.user_id) - user = users[participant.user_id] - user.participant = participant - yield user - if len(seen) >= limit: - return + user = users[participant.user_id] + if not filter_entity(user) or user.id in seen: + continue + + seen.add(participant.user_id) + user = users[participant.user_id] + user.participant = participant + yield user + if len(seen) >= limit: + return elif isinstance(entity, InputPeerChat): + # TODO We *could* apply the `filter` here ourselves full = self(GetFullChatRequest(entity.chat_id)) if _total_box: _total_box.x = len(full.full_chat.participants.participants) @@ -1254,6 +1278,9 @@ class TelegramClient(TelegramBareClient): have = 0 users = {user.id: user for user in full.users} for participant in full.full_chat.participants.participants: + user = users[participant.user_id] + if not filter_entity(user): + continue have += 1 if have > limit: break @@ -1266,8 +1293,9 @@ class TelegramClient(TelegramBareClient): _total_box.x = 1 if limit != 0: user = self.get_entity(entity) - user.participant = None - yield user + if filter_entity(user): + user.participant = None + yield user def get_participants(self, *args, **kwargs): """ From 7e9d19d727663336534d5d1e1ffe71ecf2a66bf9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 10:28:21 +0100 Subject: [PATCH 306/361] Add known entities to all updates and use them in the events This should reduce the amount of API calls made when getting the full sender/chat on events (mostly on channels, where Telegram seems to always send Updates instead only a normal Update). --- telethon/events/__init__.py | 73 +++++++++++++++++++++++++++++-------- telethon/update_state.py | 15 ++++++-- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index a4ec83b2..8047bb49 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -71,6 +71,7 @@ class _EventCommon(abc.ABC): """Intermediate class with common things to all events""" def __init__(self, chat_peer=None, msg_id=None, broadcast=False): + self._entities = {} self._client = None self._chat_peer = chat_peer self._message_id = msg_id @@ -104,6 +105,7 @@ class _EventCommon(abc.ABC): ) except RPCError: return + # TODO This could return a tuple to also have the full entity entity = { utils.get_peer_id(x): x for x in itertools.chain( getattr(result, 'chats', []), @@ -148,12 +150,19 @@ class _EventCommon(abc.ABC): def chat(self): """ The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which - the event occurred. This property will make an API call the first time - to get the most up to date version of the chat, so use with care as - there is no caching besides local caching yet. + the event occurred. This property may make an API call the first time + to get the most up to date version of the chat (mostly when the event + doesn't belong to a channel), so keep that in mind. """ - if self._chat is None and self.input_chat: + if not self.input_chat: + return None + + if self._chat is None: + self._chat = self._entities.get(utils.get_peer_id(self._input_chat)) + + if self._chat is None: self._chat = self._client.get_entity(self._input_chat) + return self._chat @@ -249,6 +258,7 @@ class NewMessage(_EventBuilder): return # Short-circuit if we let pass all events + event._entities = update.entities if all(x is None for x in (self.incoming, self.outgoing, self.chats, self.pattern)): return event @@ -300,8 +310,6 @@ class NewMessage(_EventBuilder): self.message = message self._text = None - self._input_chat = None - self._chat = None self._input_sender = None self._sender = None @@ -395,14 +403,22 @@ class NewMessage(_EventBuilder): @property def sender(self): """ - This (:obj:`User`) will make an API call the first time to get - the most up to date version of the sender, so use with care as - there is no caching besides local caching yet. + This (:obj:`User`) may make an API call the first time to get + the most up to date version of the sender (mostly when the event + doesn't belong to a channel), so keep that in mind. ``input_sender`` needs to be available (often the case). """ - if self._sender is None and self.input_sender: + if not self.input_sender: + return None + + if self._sender is None: + self._sender = \ + self._entities.get(utils.get_peer_id(self._input_sender)) + + if self._sender is None: self._sender = self._client.get_entity(self._input_sender) + return self._sender @property @@ -621,6 +637,7 @@ class ChatAction(_EventBuilder): else: return + event._entities = update.entities return self._filter_event(event) class Event(_EventCommon): @@ -762,7 +779,12 @@ class ChatAction(_EventBuilder): The user who added ``users``, if applicable (``None`` otherwise). """ if self._added_by and not isinstance(self._added_by, types.User): - self._added_by = self._client.get_entity(self._added_by) + self._added_by =\ + self._entities.get(utils.get_peer_id(self._added_by)) + + if not self._added_by: + self._added_by = self._client.get_entity(self._added_by) + return self._added_by @property @@ -771,7 +793,12 @@ class ChatAction(_EventBuilder): The user who kicked ``users``, if applicable (``None`` otherwise). """ if self._kicked_by and not isinstance(self._kicked_by, types.User): - self._kicked_by = self._client.get_entity(self._kicked_by) + self._kicked_by =\ + self._entities.get(utils.get_peer_id(self._kicked_by)) + + if not self._kicked_by: + self._kicked_by = self._client.get_entity(self._kicked_by) + return self._kicked_by @property @@ -801,11 +828,24 @@ class ChatAction(_EventBuilder): Might be empty if the information can't be retrieved or there are no users taking part. """ - if self._users is None and self._user_peers: + if not self._user_peers: + return [] + + if self._users is None: + have, missing = [], [] + for peer in self._user_peers: + user = self._entities.get(utils.get_peer_id(peer)) + if user: + have.append(user) + else: + missing.append(peer) + try: - self._users = self._client.get_entity(self._user_peers) + missing = self._client.get_entity(missing) except (TypeError, ValueError): - self._users = [] + missing = [] + + self._user_peers = have + missing return self._users @@ -837,6 +877,7 @@ class UserUpdate(_EventBuilder): else: return + event._entities = update.entities return self._filter_event(event) class Event(_EventCommon): @@ -984,6 +1025,7 @@ class MessageEdited(NewMessage): else: return + event._entities = update.entities return self._filter_event(event) @@ -1005,6 +1047,7 @@ class MessageDeleted(_EventBuilder): else: return + event._entities = update.entities return self._filter_event(event) class Event(_EventCommon): diff --git a/telethon/update_state.py b/telethon/update_state.py index 6a496603..171e9546 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -1,10 +1,10 @@ +import itertools import logging -import pickle -from collections import deque -from queue import Queue, Empty from datetime import datetime +from queue import Queue, Empty from threading import RLock, Thread +from . import utils from .tl import types as tl __log__ = logging.getLogger(__name__) @@ -127,14 +127,23 @@ class UpdateState: # After running the script for over an hour and receiving over # 1000 updates, the only duplicates received were users going # online or offline. We can trust the server until new reports. + # + # TODO Note somewhere that all updates are modified to include + # .entities, which is a dictionary you can access but may be empty. + # This should only be used as read-only. if isinstance(update, tl.UpdateShort): + update.update.entities = {} self._updates.put(update.update) # Expand "Updates" into "Update", and pass these to callbacks. # Since .users and .chats have already been processed, we # don't need to care about those either. elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): + entities = {utils.get_peer_id(x): x for x in + itertools.chain(update.users, update.chats)} for u in update.updates: + u.entities = entities self._updates.put(u) # TODO Handle "tl.UpdatesTooLong" else: + update.entities = {} self._updates.put(update) From 8ae12fbb704015f1ef11e9d73cabba7c800ce06f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 10:32:59 +0100 Subject: [PATCH 307/361] Return the entire entity from the helper events._get_entity too --- telethon/events/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 8047bb49..36020afb 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -87,12 +87,15 @@ class _EventCommon(abc.ABC): ) self.is_channel = isinstance(chat_peer, types.PeerChannel) - def _get_input_entity(self, msg_id, entity_id, chat=None): + def _get_entity(self, msg_id, entity_id, chat=None): """ Helper function to call GetMessages on the give msg_id and return the input entity whose ID is the given entity ID. If ``chat`` is present it must be an InputPeer. + + Returns a tuple of (entity, input_peer) if it was found, or + a tuple of (None, None) if it couldn't be. """ try: if isinstance(chat, types.InputPeerChannel): @@ -104,15 +107,17 @@ class _EventCommon(abc.ABC): functions.messages.GetMessagesRequest([msg_id]) ) except RPCError: - return - # TODO This could return a tuple to also have the full entity + return None, None + entity = { utils.get_peer_id(x): x for x in itertools.chain( getattr(result, 'chats', []), getattr(result, 'users', [])) }.get(entity_id) if entity: - return utils.get_input_peer(entity) + return entity, utils.get_input_peer(entity) + else: + return None, None @property def input_chat(self): @@ -136,7 +141,7 @@ class _EventCommon(abc.ABC): # TODO For channels, getDifference? Maybe looking # in the dialogs (which is already done) is enough. if self._message_id is not None: - self._input_chat = self._get_input_entity( + self._chat, self._input_chat = self._get_entity( self._message_id, utils.get_peer_id(self._chat_peer) ) @@ -392,7 +397,7 @@ class NewMessage(_EventBuilder): ) except (ValueError, TypeError): # We can rely on self.input_chat for this - self._input_sender = self._get_input_entity( + self._sender, self._input_sender = self._get_entity( self.message.id, self.message.from_id, chat=self.input_chat From 81944262fb24d46b9604f7966cf3061977fa84a3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 19:38:36 +0100 Subject: [PATCH 308/361] Clear-up documentation by separating reference from examples --- readthedocs/extra/basic/entities.rst | 8 ++++ readthedocs/extra/basic/telegram-client.rst | 13 +++---- .../extra/basic/working-with-updates.rst | 19 +++++----- readthedocs/index.rst | 3 +- readthedocs/telethon.crypto.rst | 8 ---- readthedocs/telethon.events.rst | 7 +++- readthedocs/telethon.rst | 37 ++++++++++--------- 7 files changed, 51 insertions(+), 44 deletions(-) diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index 84be3250..ce7e569a 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -10,6 +10,14 @@ The library widely uses the concept of "entities". An entity will refer to any ``User``, ``Chat`` or ``Channel`` object that the API may return in response to certain methods, such as ``GetUsersRequest``. +.. note:: + + When something "entity-like" is required, it means that you need to + provide something that can be turned into an entity. These things include, + but are not limited to, usernames, exact titles, IDs, ``Peer`` objects, + or even entire ``User``, ``Chat`` and ``Channel`` objects and even phone + numbers from people you have in your contacts. + Getting entities **************** diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst index d3375200..decb3765 100644 --- a/readthedocs/extra/basic/telegram-client.rst +++ b/readthedocs/extra/basic/telegram-client.rst @@ -8,6 +8,11 @@ TelegramClient Introduction ************ +.. note:: + + Check the :ref:`telethon-package` if you're looking for the methods + reference instead of this tutorial. + The ``TelegramClient`` is the central class of the library, the one you will be using most of the time. For this reason, it's important to know what it offers. @@ -86,13 +91,7 @@ Please refer to :ref:`accessing-the-full-api` if these aren't enough, and don't be afraid to read the source code of the InteractiveTelegramClient_ or even the TelegramClient_ itself to learn how it works. +To see the methods available in the client, see :ref:`telethon-package`. .. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py .. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py - - - -.. automodule:: telethon.telegram_client - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 3c57b792..105e11bd 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -7,7 +7,9 @@ Working with Updates The library comes with the :mod:`events` module. *Events* are an abstraction over what Telegram calls `updates`__, and are meant to ease simple and common -usage when dealing with them, since there are many updates. Let's dive in! +usage when dealing with them, since there are many updates. If you're looking +for the method reference, check :ref:`telethon-events-package`, otherwise, +let's dive in! .. note:: @@ -114,12 +116,15 @@ for example: import random - @client.on(events.NewMessage(chats='TelethonOffTopic', incoming=True)) + # Either a single item or a list of them will work for the chats. + # You can also use the IDs, Peers, or even User/Chat/Channel objects. + @client.on(events.NewMessage(chats=('TelethonChat', 'TelethonOffTopic'))) def normal_handler(event): if 'roll' in event.raw_text: event.reply(str(random.randint(1, 6))) + # Similarly, you can use incoming=True for messages that you receive @client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True)) def admin_handler(event): if event.raw_text.startswith('eval'): @@ -162,14 +167,8 @@ propagation of the update through your handlers to stop: pass -Events module -************* - -.. automodule:: telethon.events - :members: - :undoc-members: - :show-inheritance: - +Remember to check :ref:`telethon-events-package` if you're looking for +the methods reference. __ https://lonamiwebs.github.io/Telethon/types/update.html diff --git a/readthedocs/index.rst b/readthedocs/index.rst index c1d2b6ec..a3982d86 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -15,7 +15,8 @@ or use the menu on the left. Remember to read the :ref:`changelog` when you upgrade! .. important:: - If you're new here, you want to read :ref:`getting-started`. + If you're new here, you want to read :ref:`getting-started`. If you're + looking for the method reference, you should check :ref:`telethon-package`. What is this? diff --git a/readthedocs/telethon.crypto.rst b/readthedocs/telethon.crypto.rst index 3c11416d..8adf55d5 100644 --- a/readthedocs/telethon.crypto.rst +++ b/readthedocs/telethon.crypto.rst @@ -42,14 +42,6 @@ telethon\.crypto\.factorization module :undoc-members: :show-inheritance: -telethon\.crypto\.libssl module -------------------------------- - -.. automodule:: telethon.crypto.libssl - :members: - :undoc-members: - :show-inheritance: - telethon\.crypto\.rsa module ---------------------------- diff --git a/readthedocs/telethon.events.rst b/readthedocs/telethon.events.rst index 071a39bf..37ce9f48 100644 --- a/readthedocs/telethon.events.rst +++ b/readthedocs/telethon.events.rst @@ -1,4 +1,9 @@ +.. _telethon-events-package: + telethon\.events package ======================== - +.. automodule:: telethon.events + :members: + :undoc-members: + :show-inheritance: diff --git a/readthedocs/telethon.rst b/readthedocs/telethon.rst index 96becc9b..0b60b007 100644 --- a/readthedocs/telethon.rst +++ b/readthedocs/telethon.rst @@ -1,11 +1,14 @@ +.. _telethon-package: + + telethon package ================ -telethon\.helpers module ------------------------- +telethon\.telegram\_client module +--------------------------------- -.. automodule:: telethon.helpers +.. automodule:: telethon.telegram_client :members: :undoc-members: :show-inheritance: @@ -18,10 +21,18 @@ telethon\.telegram\_bare\_client module :undoc-members: :show-inheritance: -telethon\.telegram\_client module ---------------------------------- +telethon\.utils module +---------------------- -.. automodule:: telethon.telegram_client +.. automodule:: telethon.utils + :members: + :undoc-members: + :show-inheritance: + +telethon\.helpers module +------------------------ + +.. automodule:: telethon.helpers :members: :undoc-members: :show-inheritance: @@ -42,18 +53,10 @@ telethon\.update\_state module :undoc-members: :show-inheritance: -telethon\.utils module ----------------------- +telethon\.sessions module +------------------------- -.. automodule:: telethon.utils - :members: - :undoc-members: - :show-inheritance: - -telethon\.session module ------------------------- - -.. automodule:: telethon.session +.. automodule:: telethon.sessions :members: :undoc-members: :show-inheritance: From 1ff5826c26c0da5cd9dcccc1874dbd5ed03f56eb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 21:01:00 +0100 Subject: [PATCH 309/361] Call .get_dialogs only once on entity not found --- telethon/telegram_client.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 02bf918e..c3f8ba72 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -192,6 +192,9 @@ class TelegramClient(TelegramBareClient): # Sometimes we need to know who we are, cache the self peer self._self_input_peer = None + # Don't call .get_dialogs() every time a .get_entity() fails + self._called_get_dialogs = False + # endregion # region Telegram requests functions @@ -2401,16 +2404,18 @@ class TelegramClient(TelegramBareClient): # Add the mark to the peers if the user passed a Peer (not an int), # or said ID is negative. If it's negative it's been marked already. # Look in the dialogs with the hope to find it. - mark = not isinstance(peer, int) or peer < 0 - target_id = utils.get_peer_id(peer) - if mark: - for dialog in self.iter_dialogs(): - if utils.get_peer_id(dialog.entity) == target_id: - return utils.get_input_peer(dialog.entity) - else: - for dialog in self.iter_dialogs(): - if dialog.entity.id == target_id: - return utils.get_input_peer(dialog.entity) + if not self._called_get_dialogs: + self._called_get_dialogs = True + mark = not isinstance(peer, int) or peer < 0 + target_id = utils.get_peer_id(peer) + if mark: + for dialog in self.get_dialogs(100): + if utils.get_peer_id(dialog.entity) == target_id: + return utils.get_input_peer(dialog.entity) + else: + for dialog in self.get_dialogs(100): + if dialog.entity.id == target_id: + return utils.get_input_peer(dialog.entity) raise TypeError( 'Could not find the input entity corresponding to "{}". ' From d5bc3c1a6cfbb03729cf2e7318e17d405fb85902 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 21:05:50 +0100 Subject: [PATCH 310/361] Fix misleading documentation regarding report_errors --- readthedocs/extra/developing/api-status.rst | 8 +++----- telethon/telegram_bare_client.py | 1 - telethon/telegram_client.py | 5 ++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/readthedocs/extra/developing/api-status.rst b/readthedocs/extra/developing/api-status.rst index 492340a4..181ed3ae 100644 --- a/readthedocs/extra/developing/api-status.rst +++ b/readthedocs/extra/developing/api-status.rst @@ -10,11 +10,9 @@ anyone can query, made by `Daniil `__. All the information sent is a ``GET`` request with the error code, error message and method used. -If you still would like to opt out, simply set -``client.session.report_errors = False`` to disable this feature, or -pass ``report_errors=False`` as a named parameter when creating a -``TelegramClient`` instance. However Daniil would really thank you if -you helped him (and everyone) by keeping it on! +If you still would like to opt out, you can disable this feature by setting +``client.session.report_errors = False``. However Daniil would really thank +you if you helped him (and everyone) by keeping it on! Querying the API status *********************** diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 7164bb17..2ada33f5 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -73,7 +73,6 @@ class TelegramBareClient: update_workers=None, spawn_read_thread=False, timeout=timedelta(seconds=5), - loop=None, device_model=None, system_version=None, app_version=None, diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c3f8ba72..642ecbf5 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -147,8 +147,8 @@ class TelegramClient(TelegramBareClient): if you want to run the library without any additional thread. Kwargs: - Extra parameters will be forwarded to the ``Session`` file. - Most relevant parameters are: + Some extra parameters are required when stabilishing the first + connection. These are are (along with their default values): .. code-block:: python @@ -157,7 +157,6 @@ class TelegramClient(TelegramBareClient): app_version = TelegramClient.__version__ lang_code = 'en' system_lang_code = lang_code - report_errors = True """ # region Initialization From 48869f0f4e5f2b4065f916f031ef6c6e8a7dbe73 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 14 Mar 2018 21:09:51 +0100 Subject: [PATCH 311/361] Fix MessageEdited ignoring NewMessage constructor arguments These include outgoing/incoming and pattern which are now handled. --- telethon/events/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 36020afb..4339faef 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -262,8 +262,11 @@ class NewMessage(_EventBuilder): else: return - # Short-circuit if we let pass all events event._entities = update.entities + return self._message_filter_event(event) + + def _message_filter_event(self, event): + # Short-circuit if we let pass all events if all(x is None for x in (self.incoming, self.outgoing, self.chats, self.pattern)): return event @@ -1031,7 +1034,7 @@ class MessageEdited(NewMessage): return event._entities = update.entities - return self._filter_event(event) + return self._message_filter_event(event) class MessageDeleted(_EventBuilder): From 423f0f366c955b9e20b8b41dd374986ee4647634 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 09:29:54 +0100 Subject: [PATCH 312/361] Update test servers documentation (#700) --- readthedocs/extra/developing/test-servers.rst | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/readthedocs/extra/developing/test-servers.rst b/readthedocs/extra/developing/test-servers.rst index 2ba66897..a3288a25 100644 --- a/readthedocs/extra/developing/test-servers.rst +++ b/readthedocs/extra/developing/test-servers.rst @@ -8,8 +8,7 @@ To run Telethon on a test server, use the following code: .. code-block:: python client = TelegramClient(None, api_id, api_hash) - client.session.server_address = '149.154.167.40' - client.connect() + client.session.set_dc(dc_id, '149.154.167.40', 80) You can check your ``'test ip'`` on https://my.telegram.org. @@ -17,16 +16,20 @@ You should set ``None`` session so to ensure you're generating a new authorization key for it (it would fail if you used a session where you had previously connected to another data center). -Once you're connected, you'll likely need to ``.sign_up()``. Remember -`anyone can access the phone you +Note that port 443 might not work, so you can try with 80 instead. + +Once you're connected, you'll likely be asked to either sign in or sign up. +Remember `anyone can access the phone you choose `__, -so don't store sensitive data here: +so don't store sensitive data here. + +Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and +``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would +be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five +times, in this case, ``22222`` so we can hardcode that: .. code-block:: python - from random import randint - - dc_id = '2' # Change this to the DC id of the test server you chose - phone = '99966' + dc_id + str(randint(9999)).zfill(4) - client.send_code_request(phone) - client.sign_up(dc_id * 5, 'Some', 'Name') + client = TelegramClient(None, api_id, api_hash) + client.session.set_dc(2, '149.154.167.40', 80) + client.start(phone='9996621234', code_callback=lambda: '22222') From 45b7318f083450d0574f7a123735f7fd20342b25 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 09:52:45 +0100 Subject: [PATCH 313/361] Fix Telegram only recognises 3 image filetypes --- telethon/utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index daf8c875..ddc920d3 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -4,10 +4,11 @@ to convert between an entity like an User, Chat, etc. into its Input version) """ import math import mimetypes +import os import re import types from collections import UserList -from mimetypes import add_type, guess_extension +from mimetypes import guess_extension from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, @@ -315,11 +316,13 @@ def get_input_media(media, is_photo=False): def is_image(file): - """Returns True if the file extension looks like an image file""" + """ + Returns True if the file extension looks like an image file to Telegram. + """ if not isinstance(file, str): return False - mime = mimetypes.guess_type(file)[0] or '' - return mime.startswith('image/') and not mime.endswith('/webp') + _, ext = os.path.splitext(file) + return re.match(r'\.(png|jpe?g|gif)', ext, re.IGNORECASE) def is_audio(file): From d7ef0f5e0908417b259b4716435cc397c489d0c7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 10:13:57 +0100 Subject: [PATCH 314/361] Stop sending gifs as images This is often not the case, most gifs are animated and when sent as images inside albums they lose the animation. --- telethon/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/utils.py b/telethon/utils.py index ddc920d3..286853ad 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -322,7 +322,7 @@ def is_image(file): if not isinstance(file, str): return False _, ext = os.path.splitext(file) - return re.match(r'\.(png|jpe?g|gif)', ext, re.IGNORECASE) + return re.match(r'\.(png|jpe?g)', ext, re.IGNORECASE) def is_audio(file): From 3b42bc99912b26348e41d5b26f56c0c0e65e939c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 10:17:45 +0100 Subject: [PATCH 315/361] Slice albums larger than 10 items and allow mixing docs --- telethon/telegram_client.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 642ecbf5..93a9793a 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1342,6 +1342,10 @@ class TelegramClient(TelegramBareClient): photo or similar) so that it can be resent without the need to download and re-upload it again. + If a list or similar is provided, the files in it will be + sent as an album in the order in which they appear, sliced + in chunks of 10 if more than 10 are given. + caption (:obj:`str`, optional): Optional caption for the sent media message. @@ -1387,23 +1391,33 @@ class TelegramClient(TelegramBareClient): # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. if utils.is_list_like(file): - # Convert to tuple so we can iterate several times - file = tuple(x for x in file) - if all(utils.is_image(x) for x in file): - return self._send_album( - entity, file, caption=caption, + # TODO Fix progress_callback + images = [] + documents = [] + for x in file: + if utils.is_image(x): + images.append(x) + else: + documents.append(x) + + result = [] + while images: + result += self._send_album( + entity, images[:10], caption=caption, progress_callback=progress_callback, reply_to=reply_to, parse_mode=parse_mode ) - # Not all are images, so send all the files one by one - return [ + images = images[10:] + + result.extend( self.send_file( entity, x, allow_cache=False, caption=caption, force_document=force_document, progress_callback=progress_callback, reply_to=reply_to, attributes=attributes, thumb=thumb, **kwargs - ) for x in file - ] + ) for x in documents + ) + return result entity = self.get_input_entity(entity) reply_to = self._get_message_id(reply_to) @@ -1543,6 +1557,10 @@ class TelegramClient(TelegramBareClient): # we need to produce right now to send albums (uploadMedia), and # cache only makes a difference for documents where the user may # want the attributes used on them to change. + # + # In theory documents can be sent inside the albums but they appear + # as different messages (not inside the album), and the logic to set + # the attributes/avoid cache is already written in .send_file(). entity = self.get_input_entity(entity) if not utils.is_list_like(caption): caption = (caption,) From 1e3120b0b66b82003bd0448ea5dc713bdd89c134 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 10:22:21 +0100 Subject: [PATCH 316/361] Bring back report_errors to the constructor --- readthedocs/extra/developing/api-status.rst | 2 ++ telethon/telegram_bare_client.py | 2 ++ telethon/telegram_client.py | 8 +++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/developing/api-status.rst b/readthedocs/extra/developing/api-status.rst index 181ed3ae..e113c48e 100644 --- a/readthedocs/extra/developing/api-status.rst +++ b/readthedocs/extra/developing/api-status.rst @@ -1,3 +1,5 @@ +.. _api-status: + ========== API Status ========== diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 2ada33f5..8281f869 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -73,6 +73,7 @@ class TelegramBareClient: update_workers=None, spawn_read_thread=False, timeout=timedelta(seconds=5), + report_errors=True, device_model=None, system_version=None, app_version=None, @@ -103,6 +104,7 @@ class TelegramBareClient: DEFAULT_PORT ) + session.report_errors = report_errors self.session = session self.api_id = int(api_id) self.api_hash = api_hash diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 93a9793a..f0e24918 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -146,8 +146,12 @@ class TelegramClient(TelegramBareClient): instantly, as soon as they arrive. Can still be disabled if you want to run the library without any additional thread. + report_errors (:obj:`bool`, optional): + Whether to report RPC errors or not. Defaults to ``True``, + see :ref:`api-status` for more information. + Kwargs: - Some extra parameters are required when stabilishing the first + Some extra parameters are required when establishing the first connection. These are are (along with their default values): .. code-block:: python @@ -168,6 +172,7 @@ class TelegramClient(TelegramBareClient): update_workers=None, timeout=timedelta(seconds=5), spawn_read_thread=True, + report_errors=True, **kwargs): super().__init__( session, api_id, api_hash, @@ -177,6 +182,7 @@ class TelegramClient(TelegramBareClient): update_workers=update_workers, spawn_read_thread=spawn_read_thread, timeout=timeout, + report_errors=report_errors, **kwargs ) From b20aa0ccc91b3d767c26702f3611c44772d87f5a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Mar 2018 10:29:12 +0100 Subject: [PATCH 317/361] Stopping workers should not clear their count (may fix #686) --- telethon/update_state.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/update_state.py b/telethon/update_state.py index 171e9546..9f26e3a4 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -80,6 +80,7 @@ class UpdateState: t.join() self._worker_threads.clear() + self._workers = n def setup_workers(self): if self._worker_threads or not self._workers: From d6c051fd52eccb798a1168c004b3c9ad22525fa9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Mar 2018 09:54:16 +0100 Subject: [PATCH 318/361] Add __str__ and .stringify() to events --- telethon/events/__init__.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 4339faef..b0f21930 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -2,11 +2,12 @@ import abc import datetime import itertools import re +import warnings from .. import utils from ..errors import RPCError from ..extensions import markdown -from ..tl import types, functions +from ..tl import TLObject, types, functions def _into_id_set(client, chats): @@ -170,6 +171,17 @@ class _EventCommon(abc.ABC): return self._chat + def __str__(self): + return TLObject.pretty_format(self.to_dict()) + + def stringify(self): + return TLObject.pretty_format(self.to_dict(), indent=0) + + def to_dict(self): + d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} + d['_'] = self.__class__.__name__ + return d + class Raw(_EventBuilder): """ @@ -182,9 +194,19 @@ class Raw(_EventBuilder): return update +def _name_inner_event(cls): + """Decorator to rename cls.Event 'Event' as 'cls.Event'""" + if hasattr(cls, 'Event'): + cls.Event.__name__ = '{}.Event'.format(cls.__name__) + else: + warnings.warn('Class {} does not have a inner Event'.format(cls)) + return cls + + # Classes defined here are actually Event builders # for their inner Event classes. Inner ._client is # set later by the creator TelegramClient. +@_name_inner_event class NewMessage(_EventBuilder): """ Represents a new message event builder. @@ -580,6 +602,7 @@ class NewMessage(_EventBuilder): return self.message.out +@_name_inner_event class ChatAction(_EventBuilder): """ Represents an action in a chat (such as user joined, left, or new pin). @@ -874,6 +897,7 @@ class ChatAction(_EventBuilder): return self._input_users +@_name_inner_event class UserUpdate(_EventBuilder): """ Represents an user update (gone online, offline, joined Telegram). @@ -1022,6 +1046,7 @@ class UserUpdate(_EventBuilder): return self.chat +@_name_inner_event class MessageEdited(NewMessage): """ Event fired when a message has been edited. @@ -1037,6 +1062,7 @@ class MessageEdited(NewMessage): return self._message_filter_event(event) +@_name_inner_event class MessageDeleted(_EventBuilder): """ Event fired when one or more messages are deleted. From 36b09a9459798d8ad2712bde6bb350516fe9994a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Mar 2018 09:57:37 +0100 Subject: [PATCH 319/361] .download_file's file is not optional --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f0e24918..0d2026b6 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2067,7 +2067,7 @@ class TelegramClient(TelegramBareClient): input_location (:obj:`InputFileLocation`): The file location from which the file will be downloaded. - file (:obj:`str` | :obj:`file`, optional): + file (:obj:`str` | :obj:`file`): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. From 50256e23e9cbf555a2992fbd538bc4b1ec1bc5fe Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 17 Mar 2018 17:35:41 +0200 Subject: [PATCH 320/361] Add addon-style session dev instructions (#698) --- readthedocs/extra/advanced-usage/sessions.rst | 75 +++---------------- 1 file changed, 12 insertions(+), 63 deletions(-) diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index 66fa0560..592d8334 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -36,77 +36,26 @@ one of the other implementations or implement your own storage. To use a custom session storage, simply pass the custom session instance to ``TelegramClient`` instead of the session name. -Currently, there are three implementations of the abstract ``Session`` class: -* ``MemorySession``. Stores session data in Python variables. -* ``SQLiteSession``, (default). Stores sessions in their own SQLite databases. -* ``AlchemySession``. Stores all sessions in a single database via SQLAlchemy. +Telethon contains two implementations of the abstract ``Session`` class: -Using AlchemySession -~~~~~~~~~~~~~~~~~~~~ -The ``AlchemySession`` implementation can store multiple Sessions in the same -database, but to do this, each session instance needs to have access to the -same models and database session. +* ``MemorySession``: stores session data in Python variables. +* ``SQLiteSession``, (default): stores sessions in their own SQLite databases. -To get started, you need to create an ``AlchemySessionContainer`` which will -contain that shared data. The simplest way to use ``AlchemySessionContainer`` -is to simply pass it the database URL: +There are other community-maintained implementations available: - .. code-block:: python - - container = AlchemySessionContainer('mysql://user:pass@localhost/telethon') - -If you already have SQLAlchemy set up for your own project, you can also pass -the engine separately: - - .. code-block:: python - - my_sqlalchemy_engine = sqlalchemy.create_engine('...') - container = AlchemySessionContainer(engine=my_sqlalchemy_engine) - -By default, the session container will manage table creation/schema updates/etc -automatically. If you want to manage everything yourself, you can pass your -SQLAlchemy Session and ``declarative_base`` instances and set ``manage_tables`` -to ``False``: - - .. code-block:: python - - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy import orm - import sqlalchemy - - ... - - session_factory = orm.sessionmaker(bind=my_sqlalchemy_engine) - session = session_factory() - my_base = declarative_base() - - ... - - container = AlchemySessionContainer( - session=session, table_base=my_base, manage_tables=False - ) - -You always need to provide either ``engine`` or ``session`` to the container. -If you set ``manage_tables=False`` and provide a ``session``, ``engine`` is not -needed. In any other case, ``engine`` is always required. - -After you have your ``AlchemySessionContainer`` instance created, you can -create new sessions by calling ``new_session``: - - .. code-block:: python - - session = container.new_session('some session id') - client = TelegramClient(session) - -where ``some session id`` is an unique identifier for the session. +* `SQLAlchemy `_: stores all sessions in a single database via SQLAlchemy. +* `Redis `_: stores all sessions in a single Redis data store. Creating your own storage ~~~~~~~~~~~~~~~~~~~~~~~~~ -The easiest way to create your own implementation is to use ``MemorySession`` -as the base and check out how ``SQLiteSession`` or ``AlchemySession`` work. -You can find the relevant Python files under the ``sessions`` directory. +The easiest way to create your own storage implementation is to use ``MemorySession`` +as the base and check out how ``SQLiteSession`` or one of the community-maintained +implementations work. You can find the relevant Python files under the ``sessions`` +directory in Telethon. +After you have made your own implementation, you can add it to the community-maintained +session implementation list above with a pull request. SQLite Sessions and Heroku -------------------------- From dc07d6507533fed8b705bb0ecea4ead49bd25290 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Mar 2018 17:03:22 +0100 Subject: [PATCH 321/361] Add remove_event_handler and list_event_handlers --- telethon/telegram_client.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 0d2026b6..e4647a8e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2239,22 +2239,46 @@ class TelegramClient(TelegramBareClient): self._event_builders.append((event, callback)) + def remove_event_handler(self, callback, event=None): + """ + Inverse operation of :meth:`add_event_handler`. + + If no event is given, all events for this callback are removed. + Returns how many callbacks were removed. + """ + found = 0 + if event and not isinstance(event, type): + event = type(event) + + for i, ec in enumerate(self._event_builders): + ev, cb = ec + if cb == callback and (not event or isinstance(ev, event)): + del self._event_builders[i] + found += 1 + + return found + + def list_event_handlers(self): + """ + Lists all added event handlers, returning a list of pairs + consisting of (callback, event). + """ + return [(callback, event) for event, callback in self._event_builders] + 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""" warnings.warn( 'add_update_handler is deprecated, use the @client.on syntax ' 'or add_event_handler(callback, events.Raw) instead (see ' 'https://telethon.rtfd.io/en/latest/extra/basic/working-' 'with-updates.html)' ) - self.add_event_handler(handler, events.Raw) + return self.add_event_handler(handler, events.Raw) def remove_update_handler(self, handler): - pass + return self.remove_event_handler(handler) def list_update_handlers(self): - return [] + return [callback for callback, _ in self.list_event_handlers()] # endregion From 6f820cce97997d317292d3a5973555d8ef3921bb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Mar 2018 17:07:56 +0100 Subject: [PATCH 322/361] Mention add_event_handler in the docs and fix small nit --- readthedocs/extra/basic/working-with-updates.rst | 14 ++++++++++++++ .../developing/telegram-api-in-other-languages.rst | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 105e11bd..308c3a79 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -140,6 +140,20 @@ random number, while if you say ``'eval 4+4'``, you will reply with the solution. Try it! +Events without decorators +************************* + +If for any reason you can't use the ``@client.on`` syntax, don't worry. +You can call ``client.add_event_handler(callback, event)`` to achieve +the same effect. + +Similar to that method, you also have :meth:`client.remove_event_handler` +and :meth:`client.list_event_handlers` which do as they names indicate. + +The ``event`` type is optional in all methods and defaults to ``events.Raw`` +for adding, and ``None`` when removing (so all callbacks would be removed). + + Stopping propagation of Updates ******************************* diff --git a/readthedocs/extra/developing/telegram-api-in-other-languages.rst b/readthedocs/extra/developing/telegram-api-in-other-languages.rst index 7637282e..22bb416a 100644 --- a/readthedocs/extra/developing/telegram-api-in-other-languages.rst +++ b/readthedocs/extra/developing/telegram-api-in-other-languages.rst @@ -38,9 +38,9 @@ Kotlin ****** `Kotlogram `__ is a Telegram -implementation written in Kotlin (the now +implementation written in Kotlin (one of the `official `__ -language for +languages for `Android `__) by `@badoualy `__, currently as a beta– yet working. From 32fd64d655f3f300d01c2ac6d642dab0e00cf22b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Mar 2018 17:38:16 +0100 Subject: [PATCH 323/361] Remove SQLAlchemy session --- optional-requirements.txt | 1 - setup.py | 3 +- telethon/sessions/__init__.py | 1 - telethon/sessions/sqlalchemy.py | 236 -------------------------------- 4 files changed, 1 insertion(+), 240 deletions(-) delete mode 100644 telethon/sessions/sqlalchemy.py diff --git a/optional-requirements.txt b/optional-requirements.txt index fb83c1ab..55bfc014 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,4 +1,3 @@ cryptg pysocks hachoir3 -sqlalchemy diff --git a/setup.py b/setup.py index 05ca9197..0d6d757c 100755 --- a/setup.py +++ b/setup.py @@ -156,8 +156,7 @@ def main(): install_requires=['pyaes', 'rsa', 'typing' if version_info < (3, 5) else ""], extras_require={ - 'cryptg': ['cryptg'], - 'sqlalchemy': ['sqlalchemy'] + 'cryptg': ['cryptg'] } ) diff --git a/telethon/sessions/__init__.py b/telethon/sessions/__init__.py index a487a4bd..af3423f3 100644 --- a/telethon/sessions/__init__.py +++ b/telethon/sessions/__init__.py @@ -1,4 +1,3 @@ from .abstract import Session from .memory import MemorySession from .sqlite import SQLiteSession -from .sqlalchemy import AlchemySessionContainer, AlchemySession diff --git a/telethon/sessions/sqlalchemy.py b/telethon/sessions/sqlalchemy.py deleted file mode 100644 index a24df485..00000000 --- a/telethon/sessions/sqlalchemy.py +++ /dev/null @@ -1,236 +0,0 @@ -try: - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy import Column, String, Integer, LargeBinary, orm - import sqlalchemy as sql -except ImportError: - sql = None - -from .memory import MemorySession, _SentFileType -from .. import utils -from ..crypto import AuthKey -from ..tl.types import ( - InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel -) - -LATEST_VERSION = 1 - - -class AlchemySessionContainer: - def __init__(self, engine=None, session=None, table_prefix='', - table_base=None, manage_tables=True): - if not sql: - raise ImportError('SQLAlchemy not imported') - if isinstance(engine, str): - engine = sql.create_engine(engine) - - self.db_engine = engine - if not session: - db_factory = orm.sessionmaker(bind=self.db_engine) - self.db = orm.scoping.scoped_session(db_factory) - else: - self.db = session - - table_base = table_base or declarative_base() - (self.Version, self.Session, self.Entity, - self.SentFile) = self.create_table_classes(self.db, table_prefix, - table_base) - - if manage_tables: - table_base.metadata.bind = self.db_engine - if not self.db_engine.dialect.has_table(self.db_engine, - self.Version.__tablename__): - table_base.metadata.create_all() - self.db.add(self.Version(version=LATEST_VERSION)) - self.db.commit() - else: - self.check_and_upgrade_database() - - @staticmethod - def create_table_classes(db, prefix, Base): - class Version(Base): - query = db.query_property() - __tablename__ = '{prefix}version'.format(prefix=prefix) - version = Column(Integer, primary_key=True) - - class Session(Base): - query = db.query_property() - __tablename__ = '{prefix}sessions'.format(prefix=prefix) - - session_id = Column(String, primary_key=True) - dc_id = Column(Integer, primary_key=True) - server_address = Column(String) - port = Column(Integer) - auth_key = Column(LargeBinary) - - class Entity(Base): - query = db.query_property() - __tablename__ = '{prefix}entities'.format(prefix=prefix) - - session_id = Column(String, primary_key=True) - id = Column(Integer, primary_key=True) - hash = Column(Integer, nullable=False) - username = Column(String) - phone = Column(Integer) - name = Column(String) - - class SentFile(Base): - query = db.query_property() - __tablename__ = '{prefix}sent_files'.format(prefix=prefix) - - session_id = Column(String, primary_key=True) - md5_digest = Column(LargeBinary, primary_key=True) - file_size = Column(Integer, primary_key=True) - type = Column(Integer, primary_key=True) - id = Column(Integer) - hash = Column(Integer) - - return Version, Session, Entity, SentFile - - def check_and_upgrade_database(self): - row = self.Version.query.all() - version = row[0].version if row else 1 - if version == LATEST_VERSION: - return - - self.Version.query.delete() - - # Implement table schema updates here and increase version - - self.db.add(self.Version(version=version)) - self.db.commit() - - def new_session(self, session_id): - return AlchemySession(self, session_id) - - def list_sessions(self): - return - - def save(self): - self.db.commit() - - -class AlchemySession(MemorySession): - def __init__(self, container, session_id): - super().__init__() - self.container = container - self.db = container.db - self.Version, self.Session, self.Entity, self.SentFile = ( - container.Version, container.Session, container.Entity, - container.SentFile) - self.session_id = session_id - self._load_session() - - def _load_session(self): - sessions = self._db_query(self.Session).all() - session = sessions[0] if sessions else None - if session: - self._dc_id = session.dc_id - self._server_address = session.server_address - self._port = session.port - self._auth_key = AuthKey(data=session.auth_key) - - def clone(self, to_instance=None): - return super().clone(MemorySession()) - - def set_dc(self, dc_id, server_address, port): - super().set_dc(dc_id, server_address, port) - self._update_session_table() - - sessions = self._db_query(self.Session).all() - session = sessions[0] if sessions else None - if session and session.auth_key: - self._auth_key = AuthKey(data=session.auth_key) - else: - self._auth_key = None - - @MemorySession.auth_key.setter - def auth_key(self, value): - self._auth_key = value - self._update_session_table() - - def _update_session_table(self): - self.Session.query.filter( - self.Session.session_id == self.session_id).delete() - new = self.Session(session_id=self.session_id, dc_id=self._dc_id, - server_address=self._server_address, - port=self._port, - auth_key=(self._auth_key.key - if self._auth_key else b'')) - self.db.merge(new) - - def _db_query(self, dbclass, *args): - return dbclass.query.filter( - dbclass.session_id == self.session_id, *args - ) - - def save(self): - self.container.save() - - def close(self): - # Nothing to do here, connection is managed by AlchemySessionContainer. - pass - - def delete(self): - self._db_query(self.Session).delete() - self._db_query(self.Entity).delete() - self._db_query(self.SentFile).delete() - - def _entity_values_to_row(self, id, hash, username, phone, name): - return self.Entity(session_id=self.session_id, id=id, hash=hash, - username=username, phone=phone, name=name) - - def process_entities(self, tlo): - rows = self._entities_to_rows(tlo) - if not rows: - return - - for row in rows: - self.db.merge(row) - self.save() - - def get_entity_rows_by_phone(self, key): - row = self._db_query(self.Entity, - self.Entity.phone == key).one_or_none() - return (row.id, row.hash) if row else None - - def get_entity_rows_by_username(self, key): - row = self._db_query(self.Entity, - self.Entity.username == key).one_or_none() - return (row.id, row.hash) if row else None - - def get_entity_rows_by_name(self, key): - row = self._db_query(self.Entity, - self.Entity.name == key).one_or_none() - return (row.id, row.hash) if row else None - - def get_entity_rows_by_id(self, key, exact=True): - if exact: - query = self._db_query(self.Entity, self.Entity.id == key) - else: - ids = ( - utils.get_peer_id(PeerUser(key)), - utils.get_peer_id(PeerChat(key)), - utils.get_peer_id(PeerChannel(key)) - ) - query = self._db_query(self.Entity, self.Entity.id in ids) - - row = query.one_or_none() - return (row.id, row.hash) if row else None - - def get_file(self, md5_digest, file_size, cls): - row = self._db_query(self.SentFile, - self.SentFile.md5_digest == md5_digest, - self.SentFile.file_size == file_size, - self.SentFile.type == _SentFileType.from_type( - cls).value).one_or_none() - return (row.id, row.hash) if row else None - - def cache_file(self, md5_digest, file_size, instance): - if not isinstance(instance, (InputDocument, InputPhoto)): - raise TypeError('Cannot cache %s instance' % type(instance)) - - self.db.merge( - self.SentFile(session_id=self.session_id, md5_digest=md5_digest, - type=_SentFileType.from_type(type(instance)).value, - id=instance.id, hash=instance.access_hash)) - self.save() From 7825994393aab11101f85bc37904eee266d2991e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 17 Mar 2018 17:38:46 +0100 Subject: [PATCH 324/361] Update to v0.18.1 --- readthedocs/extra/changelog.rst | 63 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index e8876d5c..31d58d6b 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,69 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Iterator methods (v0.18.1) +========================== + +*Published at 2018/03/17* + +All the ``.get_`` methods in the ``TelegramClient`` now have a ``.iter_`` +counterpart, so you can do operations while retrieving items from them. +For instance, you can ``client.iter_dialogs()`` and ``break`` once you +find what you're looking for instead fetching them all at once. + +Another big thing, you can get entities by just their positive ID. This +may cause some collisions (although it's very unlikely), and you can (should) +still be explicit about the type you want. However, it's a lot more convenient +and less confusing. + +Breaking changes +~~~~~~~~~~~~~~~~ + +- The library only offers the default ``SQLiteSession`` again. + See :ref:`sessions` for more on how to use a different storage from now on. + +Additions +~~~~~~~~~ + +- Events now override ``__str__`` and implement ``.stringify()``, just like + every other ``TLObject`` does. +- ``events.ChatAction`` now has :meth:`respond`, :meth:`reply` and + :meth:`delete` for the message that triggered it. +- :meth:`client.iter_participants` (and its :meth:`client.get_participants` + counterpart) now expose the ``filter`` argument, and the returned users + also expose the ``.participant`` they are. +- You can now use :meth:`client.remove_event_handler` and + :meth:`client.list_event_handlers` similar how you could with normal updates. +- New properties on ``events.NewMessage``, like ``.video_note`` and ``.gif`` + to access only specific types of documents. +- The ``Draft`` class now exposes ``.text`` and ``.raw_text``, as well as a + new :meth:`Draft.send` to send it. + +Bug fixes +~~~~~~~~~ + +- ``MessageEdited`` was ignoring ``NewMessage`` constructor arguments. +- Fixes for ``Event.delete_messages`` which wouldn't handle ``MessageService``. +- Bot API style IDs not working on :meth:`client.get_input_entity`. +- :meth:`client.download_media` didn't support ``PhotoSize``. + +Enhancements +~~~~~~~~~~~~ + +- Less RPC are made when accessing the ``.sender`` and ``.chat`` of some + events (mostly those that occur in a channel). +- You can send albums larger than 10 items (they will be sliced for you), + as well as mixing normal files with photos. +- ``TLObject`` now have Python type hints. + +Internal changes +~~~~~~~~~~~~~~~~ + +- Several documentation corrections. +- :meth:`client.get_dialogs` is only called once again when an entity is + not found to avoid flood waits. + + Sessions overhaul (v0.18) ========================= diff --git a/telethon/version.py b/telethon/version.py index 90e8bfe4..90266dbf 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.18' +__version__ = '0.18.1' From d379b2633909860e965eaa8d02eaa154f7a2cd8a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Mar 2018 10:24:48 +0100 Subject: [PATCH 325/361] Fix assignement to wrong variable on ChatAction --- telethon/events/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index b0f21930..59d15821 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -876,7 +876,7 @@ class ChatAction(_EventBuilder): except (TypeError, ValueError): missing = [] - self._user_peers = have + missing + self._users = have + missing return self._users From 986ddbe600137a19e75324ad99e56e10123f4ecc Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 18 Mar 2018 18:23:10 +0200 Subject: [PATCH 326/361] Fix forwarding messages to channels (#705) --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e4647a8e..84e52479 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -846,7 +846,7 @@ class TelegramClient(TelegramBareClient): for update in result.updates: if isinstance(update, UpdateMessageID): random_to_id[update.random_id] = update.id - elif isinstance(update, UpdateNewMessage): + elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)): id_to_message[update.message.id] = update.message return [id_to_message[random_to_id[rnd]] for rnd in req.random_id] From 35eccc0ba379b427ba0d390216c6739d7dca046f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Mar 2018 17:18:45 +0100 Subject: [PATCH 327/361] Remove unwanted binary file from #389 --- telethon_examples/anytime.png | Bin 2291 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 telethon_examples/anytime.png diff --git a/telethon_examples/anytime.png b/telethon_examples/anytime.png deleted file mode 100644 index c8663cfa21f253019a139d9c41f279b3134f0ecb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2291 zcmbu8c{J4R9>9OIG4>{dFtW_3i7^?*R(1`dE;9_Fx@BvSZ4hR-F+~ioQYgjP#+Id~ z*D@&*VJtV*-|?;m&hKIePRbI$Yme$V%Fp6A(qz5Nc@+2}B}Mb-#(3v7RYjpQyLf1SBRVCMhAgM@njso;pfh@4t@iRsaD90)UGU zkOlxofFKCa_A@{g06;L|v^xX;GeBSn6ec1H7ZaBd0`zN9rYJ7QxZhyr*)MfiD<+Z#qj%TwY<1qLSvm{n!IqL<2)3W0ONR zM@Y7I_D3CDT&ZsECp>t5MO~e{XVAJFPMVui12~S6@EjyDJr0_#P&|uEovbjtTAR)Ud^D;P>(^ zkKY(<=*F+B%jm%#vm+$m_F(6idMPJM%bWxV&tnseue8y+k;+q#bBr>JY~zd2Y9eTK zTPTDPSsYWygk8zin^panHx2`LQ?7TV*zCrOWaW8%{oC@5NJT z)~r*SQ+}MoNp(i*99rx7y)26-tV4zu#RWek+XSbkRIzzJ{l_=MH}oH^RY}y&5^T{P zH)v_yQ%KE{PQ&ZA_FXC=Z17T##&=R>|k}?EVb2~-_KypQ6E1}>Ci9h44dDLIlj=3bCv44 z(cSOj$Yi1#4Bf)CQ))(X(-I`?U%!c2XiMB$WPZ5JxfC2oE@o?`?0hj|0V&r%`Lz?| zv_sDmf6Dk!nBYx#9dosd5yy}m>J=ZK@cc8X<&3PhuBG77EmVctqzbR!lNm%afEySu zIzF10%C``F04-YNOBsHCM_-KqcKsujy&xQ|5 z=j1^;Nb0uV1Y zmMABK_JuaE@V*MVIQ80)xL1}K_qoK(mB~FSfu-5gqcFkB#aXbST8B6jf`ZU^~G;K=ft^ru#zEKRQ&n@n+s zO>OWcw1J+DVmqq4YS2E}^oUQB<7VNU;k3J@e<`mZUYeh&)T=uB$lOw|Bxh#TC$t_g zKPZ*M!Nl7RO+E6mgJWPUxIfWX3L#(AX1cJ+si^X<9bmA|%0znMn))KCk->_?`_j!f$w zWmxhHLS|+cvY}rq#GE-co@^2QYDi@pC^Jd>8 xfxJ89PP4%sY^`P_EY0doaJx|t{qfp&*Q!{0T78H=^?6vWoq;tQFK@9u`Zo{N?wSAq From 987cf41ec636f97c36dc1c43946e1626ac46894f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Mar 2018 20:01:19 +0100 Subject: [PATCH 328/361] Higher timeout and log them as warning if any data was received This might be the cause for "number of retries reached 0" so more specific logging calls might be useful. If while reading a response it times out but some data had already been read, said data will be lost. The sequence of events that triggered reaching 0 retries was: - Sending requests with IDs XYZ - socket.timeout while reading - Items timed out. Retrying - Processing RPC result - Received response for XYZ - Lost request with ID XYZ --- telethon/extensions/tcp_client.py | 12 ++++++++++-- telethon/telegram_client.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index d335e57a..1b7d0113 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -153,7 +153,6 @@ class TcpClient: if self._socket is None: self._raise_connection_reset(None) - # TODO Remove the timeout from this method, always use previous one with BufferedWriter(BytesIO(), buffer_size=size) as buffer: bytes_left = size while bytes_left != 0: @@ -162,7 +161,16 @@ class TcpClient: except socket.timeout as e: # These are somewhat common if the server has nothing # to send to us, so use a lower logging priority. - __log__.debug('socket.timeout "%s" while reading data', e) + if bytes_left < size: + __log__.warning( + 'socket.timeout "%s" when %d/%d had been received', + e, size - bytes_left, size + ) + else: + __log__.debug( + 'socket.timeout "%s" while reading data', e + ) + raise TimeoutError() from e except ConnectionError as e: __log__.info('ConnectionError "%s" while reading data', e) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 84e52479..ece8045c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -170,7 +170,7 @@ class TelegramClient(TelegramBareClient): use_ipv6=False, proxy=None, update_workers=None, - timeout=timedelta(seconds=5), + timeout=timedelta(seconds=10), spawn_read_thread=True, report_errors=True, **kwargs): From 89ae0cb164e12571435fb33e8113712ab5c65b8d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 21 Mar 2018 08:55:13 +0100 Subject: [PATCH 329/361] Make readthedocs build run without warnings --- readthedocs/extra/basic/entities.rst | 2 ++ readthedocs/extra/changelog.rst | 24 ++++++++++----------- telethon/events/__init__.py | 32 ++++++++++++++-------------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index ce7e569a..598a4230 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -1,3 +1,5 @@ +.. _entities: + ========================= Users, Chats and Channels ========================= diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 31d58d6b..a22ad725 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -993,7 +993,7 @@ Bug fixes and enhancements (v0.13.3) .. bugs-fixed-2: Bug fixes ---------- +~~~~~~~~~ - **Reconnection** used to fail because it tried invoking things from the ``ReadThread``. @@ -1009,7 +1009,7 @@ Bug fixes .. enhancements-3: Enhancements ------------- +~~~~~~~~~~~~ - **Request will be retried** up to 5 times by default rather than failing on the first attempt. @@ -1099,7 +1099,7 @@ outside the buffer. .. additions-2: Additions ---------- +~~~~~~~~~ - The mentioned different connection modes, and a new thread. - You can modify the ``Session`` attributes through the @@ -1112,7 +1112,7 @@ Additions .. enhancements-4: Enhancements ------------- +~~~~~~~~~~~~ - The low-level socket doesn't use a handcrafted timeout anymore, which should benefit by avoiding the arbitrary ``sleep(0.1)`` that there @@ -1121,7 +1121,7 @@ Enhancements ``code`` was provided. Deprecation ------------ +~~~~~~~~~~~ - ``.sign_up`` does *not* take a ``phone`` argument anymore. Change this or you will be using ``phone`` as ``code``, and it will fail! @@ -1201,7 +1201,7 @@ friendly, along with some other stability enhancements, although it brings quite a few changes. Breaking changes ----------------- +~~~~~~~~~~~~~~~~ - The ``TelegramClient`` methods ``.send_photo_file()``, ``.send_document_file()`` and ``.send_media_file()`` are now a @@ -1216,7 +1216,7 @@ Breaking changes ``.download_contact()`` still exist, but are private. Additions ---------- +~~~~~~~~~ - Updated to **layer 70**! - Both downloading and uploading now support **stream-like objects**. @@ -1232,7 +1232,7 @@ Additions .. bug-fixes-5: Bug fixes ---------- +~~~~~~~~~ - Crashing when migrating to a new layer and receiving old updates should not happen now. @@ -1372,7 +1372,7 @@ Support for parallel connections (v0.11) **read the whole change log**! Breaking changes ----------------- +~~~~~~~~~~~~~~~~ - Every Telegram error has now its **own class**, so it's easier to fine-tune your ``except``\ 's. @@ -1384,7 +1384,7 @@ Breaking changes anymore. Additions ---------- +~~~~~~~~~ - A new, more **lightweight class** has been added. The ``TelegramBareClient`` is now the base of the normal @@ -1404,7 +1404,7 @@ Additions .. bug-fixes-6: Bug fixes ---------- +~~~~~~~~~ - Received errors are acknowledged to the server, so they don't happen over and over. @@ -1418,7 +1418,7 @@ Bug fixes not happen anymore. Internal changes ----------------- +~~~~~~~~~~~~~~~~ - Some fixes to the ``JsonSession``. - Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 59d15821..d8d90bea 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1095,22 +1095,22 @@ class MessageDeleted(_EventBuilder): class StopPropagation(Exception): """ - If this Exception is found to be raised in any of the handlers for a - given update, it will stop the execution of all other registered - event handlers in the chain. - Think of it like a ``StopIteration`` exception in a for loop. + If this exception is raised in any of the handlers for a given event, + it will stop the execution of all other registered event handlers. + It can be seen as the ``StopIteration`` in a for loop but for events. Example usage: - ``` - @client.on(events.NewMessage) - def delete(event): - event.delete() - # Other handlers won't have an event to work with - raise StopPropagation - - @client.on(events.NewMessage) - def _(event): - # Will never be reached, because it is the second handler in the chain. - pass - ``` + >>> @client.on(events.NewMessage) + ... def delete(event): + ... event.delete() + ... # No other event handler will have a chance to handle this event + ... raise StopPropagation + ... + >>> @client.on(events.NewMessage) + ... def _(event): + ... # Will never be reached, because it is the second handler + ... pass """ + # For some reason Sphinx wants the silly >>> or + # it will show warnings and look bad when generated. + pass From 3550974b71087c3e66e8166b4ed623d2c160379c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 21 Mar 2018 09:17:56 +0100 Subject: [PATCH 330/361] Fix documentation for events Changing the .__name__ of a class will make it not show in the generated documentation, so instead we need to use a different variable. --- telethon/events/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d8d90bea..898f29a4 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -70,6 +70,7 @@ class _EventBuilder(abc.ABC): class _EventCommon(abc.ABC): """Intermediate class with common things to all events""" + _event_name = 'Event' def __init__(self, chat_peer=None, msg_id=None, broadcast=False): self._entities = {} @@ -179,7 +180,7 @@ class _EventCommon(abc.ABC): def to_dict(self): d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} - d['_'] = self.__class__.__name__ + d['_'] = self._event_name return d @@ -197,7 +198,7 @@ class Raw(_EventBuilder): def _name_inner_event(cls): """Decorator to rename cls.Event 'Event' as 'cls.Event'""" if hasattr(cls, 'Event'): - cls.Event.__name__ = '{}.Event'.format(cls.__name__) + cls.Event._event_name = '{}.Event'.format(cls.__name__) else: warnings.warn('Class {} does not have a inner Event'.format(cls)) return cls @@ -1061,6 +1062,9 @@ class MessageEdited(NewMessage): event._entities = update.entities return self._message_filter_event(event) + class Event(NewMessage.Event): + pass # Required if we want a different name for it + @_name_inner_event class MessageDeleted(_EventBuilder): From f2407409b37c1d11aee7bbc600e445a80183bcc7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 21 Mar 2018 09:46:57 +0100 Subject: [PATCH 331/361] Fix send_file(force_document=True) for albums (closes #713) --- telethon/telegram_client.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ece8045c..b8133d44 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1399,12 +1399,15 @@ class TelegramClient(TelegramBareClient): if utils.is_list_like(file): # TODO Fix progress_callback images = [] - documents = [] - for x in file: - if utils.is_image(x): - images.append(x) - else: - documents.append(x) + if force_document: + documents = file + else: + documents = [] + for x in file: + if utils.is_image(x): + images.append(x) + else: + documents.append(x) result = [] while images: @@ -1417,7 +1420,7 @@ class TelegramClient(TelegramBareClient): result.extend( self.send_file( - entity, x, allow_cache=False, + entity, x, allow_cache=allow_cache, caption=caption, force_document=force_document, progress_callback=progress_callback, reply_to=reply_to, attributes=attributes, thumb=thumb, **kwargs From 95f368201e53f440ba5226f2df78fd2c87cb2b7c Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 21 Mar 2018 17:01:14 +0800 Subject: [PATCH 332/361] Fix ChatAction not handling all pin events (#715) --- telethon/events/__init__.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 898f29a4..a91665fb 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -609,11 +609,12 @@ class ChatAction(_EventBuilder): Represents an action in a chat (such as user joined, left, or new pin). """ def build(self, update): - if isinstance(update, types.UpdateChannelPinnedMessage): - # Telegram sends UpdateChannelPinnedMessage and then - # UpdateNewChannelMessage with MessageActionPinMessage. + if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0: + # Telegram does not always send + # UpdateChannelPinnedMessage for new pins + # but always for unpin, with update.id = 0 event = ChatAction.Event(types.PeerChannel(update.channel_id), - new_pin=update.id) + unpin=True) elif isinstance(update, types.UpdateChatParticipantAdd): event = ChatAction.Event(types.PeerChat(update.chat_id), @@ -664,6 +665,11 @@ class ChatAction(_EventBuilder): event = ChatAction.Event(msg, users=msg.from_id, new_photo=True) + elif isinstance(action, types.MessageActionPinMessage): + # Telegram always sends this service message for new pins + event = ChatAction.Event(msg, + users=msg.from_id, + new_pin=msg.reply_to_msg_id) else: return else: @@ -678,7 +684,7 @@ class ChatAction(_EventBuilder): Members: new_pin (:obj:`bool`): - ``True`` if the pin has changed (new pin or removed). + ``True`` if there is a new pin. new_photo (:obj:`bool`): ``True`` if there's a new chat photo (or it was removed). @@ -704,10 +710,13 @@ class ChatAction(_EventBuilder): new_title (:obj:`bool`, optional): The new title string for the chat, if applicable. + + unpin (:obj:`bool`): + ``True`` if the existing pin gets unpinned. """ def __init__(self, where, new_pin=None, new_photo=None, added_by=None, kicked_by=None, created=None, - users=None, new_title=None): + users=None, new_title=None, unpin=None): if isinstance(where, types.MessageService): self.action_message = where where = where.to_id @@ -726,7 +735,7 @@ class ChatAction(_EventBuilder): self._added_by = None self._kicked_by = None self.user_added, self.user_joined, self.user_left,\ - self.user_kicked = (False, False, False, False) + self.user_kicked, self.unpin = (False, False, False, False, False) if added_by is True: self.user_joined = True @@ -745,6 +754,7 @@ class ChatAction(_EventBuilder): self._users = None self._input_users = None self.new_title = new_title + self.unpin = unpin def respond(self, *args, **kwargs): """ From dce0fd9e03e544e13a64175308c2f0de1aceb966 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 18:39:42 +0100 Subject: [PATCH 333/361] Add missing documentation for telethon.tl.custom and crosslinks --- .../extra/examples/chats-and-channels.rst | 21 ++++++++- readthedocs/telethon.tl.custom.rst | 9 ++++ telethon/telegram_client.py | 29 ++++++------ telethon/tl/custom/dialog.py | 45 ++++++++++++++++++- telethon/tl/custom/draft.py | 43 ++++++++++++++++-- 5 files changed, 126 insertions(+), 21 deletions(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 95fa1b1e..f38519c6 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -56,11 +56,12 @@ Adding someone else to such chat or channel ******************************************* If you don't want to add yourself, maybe because you're already in, -you can always add someone else with the `AddChatUserRequest`__, -which use is very straightforward: +you can always add someone else with the `AddChatUserRequest`__, which +use is very straightforward, or `InviteToChannelRequest`__ for channels: .. code-block:: python + # For normal chats from telethon.tl.functions.messages import AddChatUserRequest client(AddChatUserRequest( @@ -69,6 +70,15 @@ which use is very straightforward: fwd_limit=10 # Allow the user to see the 10 last messages )) + # For channels + from telethon.tl.functions.channels import InviteToChannelRequest + + client(InviteToChannelRequest( + channel, + [users_to_add] + )) + + Checking a link without joining ******************************* @@ -84,6 +94,7 @@ __ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html __ https://lonamiwebs.github.io/Telethon/methods/channels/index.html __ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html __ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html +__ https://lonamiwebs.github.io/Telethon/methods/channels/invite_to_channel.html __ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html @@ -225,6 +236,12 @@ use `GetMessagesViewsRequest`__, setting ``increment=True``: increment=True )) + +Note that you can only do this **once or twice a day** per account, +running this in a loop will obviously not increase the views forever +unless you wait a day between each iteration. If you run it any sooner +than that, the views simply won't be increased. + __ https://github.com/LonamiWebs/Telethon/issues/233 __ https://github.com/LonamiWebs/Telethon/issues/305 __ https://github.com/LonamiWebs/Telethon/issues/409 diff --git a/readthedocs/telethon.tl.custom.rst b/readthedocs/telethon.tl.custom.rst index a1290869..7f59596c 100644 --- a/readthedocs/telethon.tl.custom.rst +++ b/readthedocs/telethon.tl.custom.rst @@ -10,3 +10,12 @@ telethon\.tl\.custom\.draft module :undoc-members: :show-inheritance: + + +telethon\.tl\.custom\.dialog module +----------------------------------- + +.. automodule:: telethon.tl.custom.dialog + :members: + :undoc-members: + :show-inheritance: diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index b8133d44..361366d3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -102,7 +102,8 @@ class TelegramClient(TelegramBareClient): Initializes the Telegram client with the specified API ID and Hash. Args: - session (:obj:`str` | :obj:`Session` | :obj:`None`): + session (:obj:`str` | :obj:`telethon.sessions.abstract.Session`, \ + :obj:`None`): The file name of the session file to be used if a string is given (it may be a full path), or the Session instance to be used otherwise. If it's ``None``, the session will not be saved, @@ -394,7 +395,7 @@ class TelegramClient(TelegramBareClient): Returns: The signed in user, or the information about - :meth:`.send_code_request()`. + :meth:`send_code_request`. """ if self.is_user_authorized(): self._check_events_pending_resolve() @@ -550,7 +551,7 @@ class TelegramClient(TelegramBareClient): A _Box instance to pass the total parameter by reference. Yields: - Instances of ``telethon.tl.custom.Dialog``. + Instances of :obj:`telethon.tl.custom.dialog.Dialog`. """ limit = float('inf') if limit is None else int(limit) if limit == 0: @@ -606,7 +607,7 @@ class TelegramClient(TelegramBareClient): def get_dialogs(self, *args, **kwargs): """ Same as :meth:`iter_dialogs`, but returns a list instead - with an additional .total attribute on the list. + with an additional ``.total`` attribute on the list. """ total_box = _Box(0) kwargs['_total_box'] = total_box @@ -618,9 +619,10 @@ class TelegramClient(TelegramBareClient): """ Iterator over all open draft messages. - The yielded items are custom ``Draft`` objects that are easier to use. - You can call ``draft.set_message('text')`` to change the message, - or delete it through :meth:`draft.delete()`. + Instances of :obj:`telethon.tl.custom.draft.Draft` are yielded. + You can call :obj:`telethon.tl.custom.draft.Draft.set_message` + to change the message or :obj:`telethon.tl.custom.draft.Draft.delete` + among other things. """ for update in self(GetAllDraftsRequest()).updates: yield Draft._from_update(self, update) @@ -674,7 +676,7 @@ class TelegramClient(TelegramBareClient): def _parse_message_text(self, message, parse_mode): """ - Returns a (parsed message, entities) tuple depending on parse_mode. + Returns a (parsed message, entities) tuple depending on ``parse_mode``. """ if not parse_mode: return message, [] @@ -741,7 +743,7 @@ class TelegramClient(TelegramBareClient): Has no effect when sending a file. Returns: - the sent message + the sent message. """ if file is not None: return self.send_file( @@ -1001,7 +1003,6 @@ class TelegramClient(TelegramBareClient): second is the default for this limit (or above). You may need an higher limit, so you're free to set the ``batch_size`` that you think may be good. - """ entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) @@ -1079,7 +1080,7 @@ class TelegramClient(TelegramBareClient): def get_messages(self, *args, **kwargs): """ Same as :meth:`iter_messages`, but returns a list instead - with an additional .total attribute on the list. + with an additional ``.total`` attribute on the list. """ total_box = _Box(0) kwargs['_total_box'] = total_box @@ -1308,7 +1309,7 @@ class TelegramClient(TelegramBareClient): def get_participants(self, *args, **kwargs): """ Same as :meth:`iter_participants`, but returns a list instead - with an additional .total attribute on the list. + with an additional ``.total`` attribute on the list. """ total_box = _Box(0) kwargs['_total_box'] = total_box @@ -1918,7 +1919,7 @@ class TelegramClient(TelegramBareClient): return file def _download_document(self, document, file, date, progress_callback): - """Specialized version of .download_media() for documents""" + """Specialized version of .download_media() for documents.""" if isinstance(document, MessageMediaDocument): document = document.document if not isinstance(document, Document): @@ -1965,7 +1966,7 @@ class TelegramClient(TelegramBareClient): @staticmethod def _download_contact(mm_contact, file): """Specialized version of .download_media() for contacts. - Will make use of the vCard 4.0 format + Will make use of the vCard 4.0 format. """ first_name = mm_contact.first_name last_name = mm_contact.last_name diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index 366a19bf..a2b1a966 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -7,7 +7,47 @@ class Dialog: Custom class that encapsulates a dialog (an open "conversation" with someone, a group or a channel) providing an abstraction to easily access the input version/normal entity/message etc. The library will - return instances of this class when calling `client.get_dialogs()`. + return instances of this class when calling :meth:`.get_dialogs()`. + + Args: + dialog (:obj:`Dialog`): + The original ``Dialog`` instance. + + pinned (:obj:`bool`): + Whether this dialog is pinned to the top or not. + + message (:obj:`Message`): + The last message sent on this dialog. Note that this member + will not be updated when new messages arrive, it's only set + on creation of the instance. + + date (:obj:`datetime`): + The date of the last message sent on this dialog. + + entity (:obj:`entity`): + The entity that belongs to this dialog (user, chat or channel). + + input_entity (:obj:`InputPeer`): + Input version of the entity. + + id (:obj:`int`): + The marked ID of the entity, which is guaranteed to be unique. + + name (:obj:`str`): + Display name for this dialog. For chats and channels this is + their title, and for users it's "First-Name Last-Name". + + unread_count (:obj:`int`): + How many messages are currently unread in this dialog. Note that + this value won't update when new messages arrive. + + unread_mentions_count (:obj:`int`): + How many mentions are currently unread in this dialog. Note that + this value won't update when new messages arrive. + + draft (:obj:`telethon.tl.custom.draft.Draft`): + The draft object in this dialog. It will not be ``None``, + so you can call ``draft.set_message(...)``. """ def __init__(self, client, dialog, entities, messages): # Both entities and messages being dicts {ID: item} @@ -19,6 +59,7 @@ class Dialog: self.entity = entities[utils.get_peer_id(dialog.peer)] self.input_entity = utils.get_input_peer(self.entity) + self.id = utils.get_peer_id(self.input_entity) self.name = utils.get_display_name(self.entity) self.unread_count = dialog.unread_count @@ -29,6 +70,6 @@ class Dialog: def send_message(self, *args, **kwargs): """ Sends a message to this dialog. This is just a wrapper around - client.send_message(dialog.input_entity, *args, **kwargs). + ``client.send_message(dialog.input_entity, *args, **kwargs)``. """ return self._client.send_message(self.input_entity, *args, **kwargs) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index 8f3aac60..fc40c1cf 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -1,3 +1,5 @@ +import datetime + from ..functions.messages import SaveDraftRequest from ..types import UpdateDraftMessage, DraftMessage from ...extensions import markdown @@ -7,7 +9,17 @@ 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()``. + instances of this class when calling :meth:`get_drafts()`. + + Args: + date (:obj:`datetime`): + The date of the draft. + + link_preview (:obj:`bool`): + Whether the link preview is enabled or not. + + reply_to_msg_id (:obj:`int`): + The message ID that the draft will reply to. """ def __init__(self, client, peer, draft): self._client = client @@ -33,20 +45,41 @@ class Draft: @property def entity(self): + """ + The entity that belongs to this dialog (user, chat or channel). + """ return self._client.get_entity(self._peer) @property def input_entity(self): + """ + Input version of the entity. + """ return self._client.get_input_entity(self._peer) @property def text(self): + """ + The markdown text contained in the draft. It will be + empty if there is no text (and hence no draft is set). + """ return self._text @property def raw_text(self): + """ + The raw (text without formatting) contained in the draft. + It will be empty if there is no text (thus draft not set). + """ return self._raw_text + @property + def is_empty(self): + """ + Convenience bool to determine if the draft is empty or not. + """ + return not self._text + def set_message(self, text=None, reply_to=0, parse_mode='md', link_preview=None): """ @@ -88,10 +121,15 @@ class Draft: self._raw_text = raw_text self.link_preview = link_preview self.reply_to_msg_id = reply_to + self.date = datetime.datetime.now() return result def send(self, clear=True, parse_mode='md'): + """ + Sends the contents of this draft to the dialog. This is just a + wrapper around send_message(dialog.input_entity, *args, **kwargs). + """ self._client.send_message(self._peer, self.text, reply_to=self.reply_to_msg_id, link_preview=self.link_preview, @@ -100,7 +138,6 @@ class Draft: def delete(self): """ - Deletes this draft - :return bool: ``True`` on success + Deletes this draft, and returns ``True`` on success. """ return self.set_message(text='') From c71d2e18cb06c6967493f5f364ffb9d6e40502c5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 18:54:08 +0100 Subject: [PATCH 334/361] Don't perform exact search on the docs --- docs/res/js/search.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/res/js/search.js b/docs/res/js/search.js index 1b33980b..3e24bf0c 100644 --- a/docs/res/js/search.js +++ b/docs/res/js/search.js @@ -77,6 +77,37 @@ if (typeof prependPath !== 'undefined') { } } +// Assumes haystack has no whitespace and both are lowercase. +function find(haystack, needle) { + if (needle.length == 0) { + return true; + } + var hi = 0; + var ni = 0; + while (true) { + while (needle[ni] < 'a' || needle[ni] > 'z') { + ++ni; + if (ni == needle.length) { + return true; + } + } + while (haystack[hi] != needle[ni]) { + ++hi; + if (hi == haystack.length) { + return false; + } + } + ++hi; + ++ni; + if (ni == needle.length) { + return true; + } + if (hi == haystack.length) { + return false; + } + } +} + // Given two input arrays "original" and "original urls" and a query, // return a pair of arrays with matching "query" elements from "original". // @@ -86,7 +117,7 @@ function getSearchArray(original, originalu, query) { var destinationu = []; for (var i = 0; i < original.length; ++i) { - if (original[i].toLowerCase().indexOf(query) != -1) { + if (find(original[i].toLowerCase(), query)) { destination.push(original[i]); destinationu.push(originalu[i]); } From 33e908de422508d28be66cd8bb16a698ce9b54aa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 19:01:50 +0100 Subject: [PATCH 335/361] Fix markdown regex not supporting [] inside URLs --- telethon/extensions/markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index a5dde5c6..680aabda 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -21,7 +21,7 @@ DEFAULT_DELIMITERS = { '```': MessageEntityPre } -DEFAULT_URL_RE = re.compile(r'\[([^\]]+)\]\((.+?)\)') +DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)') DEFAULT_URL_FORMAT = '[{0}]({1})' From 09c04282c9c67a98cc1aa9ecfdd3a921d202ff30 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 19:02:40 +0100 Subject: [PATCH 336/361] Fix typing dependency must be installed below Python 3.5.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0d6d757c..7178c131 100755 --- a/setup.py +++ b/setup.py @@ -154,7 +154,7 @@ def main(): 'telethon_generator/parser/tl_parser.py', ]), install_requires=['pyaes', 'rsa', - 'typing' if version_info < (3, 5) else ""], + 'typing' if version_info < (3, 5, 2) else ""], extras_require={ 'cryptg': ['cryptg'] } From 021cb21686e0b9171cc99df4a4d4ae93d563c139 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 19:13:42 +0100 Subject: [PATCH 337/361] Replace custom Box class with a single-item list for args by ref --- telethon/telegram_client.py | 70 +++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 361366d3..a4ae4ebc 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -91,12 +91,6 @@ from .extensions import markdown, html __log__ = logging.getLogger(__name__) -class _Box: - """Helper class to pass parameters by reference""" - def __init__(self, x=None): - self.x = x - - class TelegramClient(TelegramBareClient): """ Initializes the Telegram client with the specified API ID and Hash. @@ -525,7 +519,7 @@ class TelegramClient(TelegramBareClient): # region Dialogs ("chats") requests def iter_dialogs(self, limit=None, offset_date=None, offset_id=0, - offset_peer=InputPeerEmpty(), _total_box=None): + offset_peer=InputPeerEmpty(), _total=None): """ Returns an iterator over the dialogs, yielding 'limit' at most. Dialogs are the open "chats" or conversations with other people. @@ -547,15 +541,15 @@ class TelegramClient(TelegramBareClient): offset_peer (:obj:`InputPeer`, optional): The peer to be used as an offset. - _total_box (:obj:`_Box`, optional): - A _Box instance to pass the total parameter by reference. + _total (:obj:`list`, optional): + A single-item list to pass the total parameter by reference. Yields: Instances of :obj:`telethon.tl.custom.dialog.Dialog`. """ limit = float('inf') if limit is None else int(limit) if limit == 0: - if not _total_box: + if not _total: return # Special case, get a single dialog and determine count dialogs = self(GetDialogsRequest( @@ -564,7 +558,7 @@ class TelegramClient(TelegramBareClient): offset_peer=offset_peer, limit=1 )) - _total_box.x = getattr(dialogs, 'count', len(dialogs.dialogs)) + _total[0] = getattr(dialogs, 'count', len(dialogs.dialogs)) return seen = set() @@ -578,8 +572,8 @@ class TelegramClient(TelegramBareClient): req.limit = min(limit - len(seen), 100) r = self(req) - if _total_box: - _total_box.x = getattr(r, 'count', len(r.dialogs)) + if _total: + _total[0] = getattr(r, 'count', len(r.dialogs)) messages = {m.id: m for m in r.messages} entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} @@ -609,10 +603,10 @@ class TelegramClient(TelegramBareClient): Same as :meth:`iter_dialogs`, but returns a list instead with an additional ``.total`` attribute on the list. """ - total_box = _Box(0) - kwargs['_total_box'] = total_box + total = [0] + kwargs['_total'] = total dialogs = UserList(self.iter_dialogs(*args, **kwargs)) - dialogs.total = total_box.x + dialogs.total = total[0] return dialogs def iter_drafts(self): # TODO: Ability to provide a `filter` @@ -940,7 +934,7 @@ class TelegramClient(TelegramBareClient): def iter_messages(self, entity, limit=20, offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0, - batch_size=100, wait_time=None, _total_box=None): + batch_size=100, wait_time=None, _total=None): """ Iterator over the message history for the specified entity. @@ -986,8 +980,8 @@ class TelegramClient(TelegramBareClient): If left to ``None``, it will default to 1 second only if the limit is higher than 3000. - _total_box (:obj:`_Box`, optional): - A _Box instance to pass the total parameter by reference. + _total (:obj:`list`, optional): + A single-item list to pass the total parameter by reference. Yields: Instances of ``telethon.tl.types.Message`` with extra attributes: @@ -1007,7 +1001,7 @@ class TelegramClient(TelegramBareClient): entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) if limit == 0: - if not _total_box: + if not _total: return # No messages, but we still need to know the total message count result = self(GetHistoryRequest( @@ -1015,7 +1009,7 @@ class TelegramClient(TelegramBareClient): offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0, hash=0 )) - _total_box.x = getattr(result, 'count', len(result.messages)) + _total[0] = getattr(result, 'count', len(result.messages)) return if wait_time is None: @@ -1036,8 +1030,8 @@ class TelegramClient(TelegramBareClient): add_offset=add_offset, hash=0 )) - if _total_box: - _total_box.x = getattr(r, 'count', len(r.messages)) + if _total: + _total[0] = getattr(r, 'count', len(r.messages)) entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} @@ -1082,10 +1076,10 @@ class TelegramClient(TelegramBareClient): Same as :meth:`iter_messages`, but returns a list instead with an additional ``.total`` attribute on the list. """ - total_box = _Box(0) - kwargs['_total_box'] = total_box + total = [0] + kwargs['_total'] = total msgs = UserList(self.iter_messages(*args, **kwargs)) - msgs.total = total_box.x + msgs.total = total[0] return msgs def get_message_history(self, *args, **kwargs): @@ -1161,7 +1155,7 @@ class TelegramClient(TelegramBareClient): raise TypeError('Invalid message type: {}'.format(type(message))) def iter_participants(self, entity, limit=None, search='', - filter=None, aggressive=False, _total_box=None): + filter=None, aggressive=False, _total=None): """ Iterator over the participants belonging to the specified chat. @@ -1191,8 +1185,8 @@ class TelegramClient(TelegramBareClient): This has no effect for groups or channels with less than 10,000 members, or if a ``filter`` is given. - _total_box (:obj:`_Box`, optional): - A _Box instance to pass the total parameter by reference. + _total (:obj:`list`, optional): + A single-item list to pass the total parameter by reference. Yields: The ``User`` objects returned by ``GetParticipantsRequest`` @@ -1220,8 +1214,8 @@ class TelegramClient(TelegramBareClient): total = self(GetFullChannelRequest( entity )).full_chat.participants_count - if _total_box: - _total_box.x = total + if _total: + _total[0] = total if limit == 0: return @@ -1281,8 +1275,8 @@ class TelegramClient(TelegramBareClient): elif isinstance(entity, InputPeerChat): # TODO We *could* apply the `filter` here ourselves full = self(GetFullChatRequest(entity.chat_id)) - if _total_box: - _total_box.x = len(full.full_chat.participants.participants) + if _total: + _total[0] = len(full.full_chat.participants.participants) have = 0 users = {user.id: user for user in full.users} @@ -1298,8 +1292,8 @@ class TelegramClient(TelegramBareClient): user.participant = participant yield user else: - if _total_box: - _total_box.x = 1 + if _total: + _total[0] = 1 if limit != 0: user = self.get_entity(entity) if filter_entity(user): @@ -1311,10 +1305,10 @@ class TelegramClient(TelegramBareClient): Same as :meth:`iter_participants`, but returns a list instead with an additional ``.total`` attribute on the list. """ - total_box = _Box(0) - kwargs['_total_box'] = total_box + total = [0] + kwargs['_total'] = total participants = UserList(self.iter_participants(*args, **kwargs)) - participants.total = total_box.x + participants.total = total[0] return participants # endregion From c6d821910ec35663a752ff3e6bcf59878bdaaf75 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 19:20:35 +0100 Subject: [PATCH 338/361] Mention that codes can expire immediately --- readthedocs/extra/basic/creating-a-client.rst | 8 ++++++++ telethon/telegram_client.py | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index e68f170b..384ebd47 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -60,6 +60,14 @@ If you're not authorized, you need to ``.sign_in()``: # If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...) # You can import both exceptions from telethon.errors. +.. note:: + + If you send the code that Telegram sent you over the app through the + app itself, it will expire immediately. You can still send the code + through the app by "obfuscating" it (maybe add a magic constant, like + ``12345``, and then subtract it to get the real code back) or any other + technique. + ``myself`` is your Telegram user. You can view all the information about yourself by doing ``print(myself.stringify())``. You're now ready to use the client as you wish! Remember that any object returned by the API has diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a4ae4ebc..6a3822d5 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -373,7 +373,10 @@ class TelegramClient(TelegramBareClient): these requests. code (:obj:`str` | :obj:`int`): - The code that Telegram sent. + The code that Telegram sent. Note that if you have sent this + code through the application itself it will immediately + expire. If you want to send the code, obfuscate it somehow. + If you're not doing any of this you can ignore this note. password (:obj:`str`): 2FA password, should be used if a previous call raised From 43c6896481fa8845d06adb174cd65c8f3b0e1bba Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Mar 2018 21:40:24 +0100 Subject: [PATCH 339/361] Add a custom role for TL references and make use of it --- readthedocs/conf.py | 14 +-- readthedocs/custom_roles.py | 69 +++++++++++++++ .../advanced-usage/accessing-the-full-api.rst | 13 ++- readthedocs/extra/basic/entities.rst | 24 +++--- readthedocs/extra/changelog.rst | 2 +- telethon/events/__init__.py | 54 ++++++------ telethon/network/mtproto_sender.py | 14 +-- telethon/telegram_client.py | 86 +++++++++---------- telethon/tl/custom/dialog.py | 6 +- telethon/tl/custom/draft.py | 2 +- telethon/update_state.py | 5 +- telethon/utils.py | 57 ++++++------ 12 files changed, 212 insertions(+), 134 deletions(-) create mode 100644 readthedocs/custom_roles.py diff --git a/readthedocs/conf.py b/readthedocs/conf.py index efb14992..35dadb24 100644 --- a/readthedocs/conf.py +++ b/readthedocs/conf.py @@ -17,15 +17,16 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) -import os import re +import os +import sys +sys.path.insert(0, os.path.abspath('.')) root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir)) +tl_ref_url = 'https://lonamiwebs.github.io/Telethon' + # -- General configuration ------------------------------------------------ @@ -36,7 +37,10 @@ root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir)) # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc'] +extensions = [ + 'sphinx.ext.autodoc', + 'custom_roles' +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/readthedocs/custom_roles.py b/readthedocs/custom_roles.py new file mode 100644 index 00000000..89a5bd79 --- /dev/null +++ b/readthedocs/custom_roles.py @@ -0,0 +1,69 @@ +from docutils import nodes, utils +from docutils.parsers.rst.roles import set_classes + + +def make_link_node(rawtext, app, name, options): + """ + Create a link to the TL reference. + + :param rawtext: Text being replaced with link node. + :param app: Sphinx application context + :param name: Name of the object to link to + :param options: Options dictionary passed to role func. + """ + try: + base = app.config.tl_ref_url + if not base: + raise AttributeError + except AttributeError as e: + raise ValueError('tl_ref_url config value is not set') from e + + if base[-1] != '/': + base += '/' + + set_classes(options) + node = nodes.reference(rawtext, utils.unescape(name), + refuri='{}?q={}'.format(base, name), + **options) + return node + + +def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None): + """ + Link to the TL reference. + + Returns 2 part tuple containing list of nodes to insert into the + document and a list of system messages. Both are allowed to be empty. + + :param name: The role name used in the document. + :param rawtext: The entire markup snippet, with role. + :param text: The text marked with the role. + :param lineno: The line number where rawtext appears in the input. + :param inliner: The inliner instance that called us. + :param options: Directive options for customization. + :param content: The directive content for customization. + """ + if options is None: + options = {} + if content is None: + content = [] + + # TODO Report error on type not found? + # Usage: + # msg = inliner.reporter.error(..., line=lineno) + # return [inliner.problematic(rawtext, rawtext, msg)], [msg] + app = inliner.document.settings.env.app + node = make_link_node(rawtext, app, text, options) + return [node], [] + + +def setup(app): + """ + Install the plugin. + + :param app: Sphinx application context. + """ + app.info('Initializing TL reference plugin') + app.add_role('tl', tl_role) + app.add_config_value('tl_ref_url', None, 'env') + return diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index edbe821d..b8d63eb6 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -25,7 +25,7 @@ You should also refer to the documentation to see what the objects from a common type, and that's the reason for this distinction. Say ``client.send_message()`` didn't exist, we could use the `search`__ -to look for "message". There we would find `SendMessageRequest`__, +to look for "message". There we would find :tl:`SendMessageRequest`, which we can work with. Every request is a Python class, and has the parameters needed for you @@ -45,11 +45,11 @@ If you're going to use a lot of these, you may do: # We now have access to 'functions.messages.SendMessageRequest' We see that this request must take at least two parameters, a ``peer`` -of type `InputPeer`__, and a ``message`` which is just a Python +of type :tl:`InputPeer`, and a ``message`` which is just a Python ``str``\ ing. -How can we retrieve this ``InputPeer``? We have two options. We manually -`construct one`__, for instance: +How can we retrieve this :tl:`InputPeer`? We have two options. We manually +construct one, for instance: .. code-block:: python @@ -64,7 +64,7 @@ Or we call ``.get_input_entity()``: peer = client.get_input_entity('someone') When you're going to invoke an API method, most require you to pass an -``InputUser``, ``InputChat``, or so on, this is why using +:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using ``.get_input_entity()`` is more straightforward (and often immediate, if you've seen the user before, know their ID, etc.). If you also need to have information about the whole user, use @@ -138,6 +138,3 @@ This can further be simplified to: __ https://lonamiwebs.github.io/Telethon __ https://lonamiwebs.github.io/Telethon/methods/index.html __ https://lonamiwebs.github.io/Telethon/?q=message -__ https://lonamiwebs.github.io/Telethon/methods/messages/send_message.html -__ https://lonamiwebs.github.io/Telethon/types/input_peer.html -__ https://lonamiwebs.github.io/Telethon/constructors/input_peer_user.html diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index 598a4230..c7e55524 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -9,16 +9,16 @@ Introduction ************ The library widely uses the concept of "entities". An entity will refer -to any ``User``, ``Chat`` or ``Channel`` object that the API may return -in response to certain methods, such as ``GetUsersRequest``. +to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return +in response to certain methods, such as :tl:`GetUsersRequest`. .. note:: When something "entity-like" is required, it means that you need to provide something that can be turned into an entity. These things include, - but are not limited to, usernames, exact titles, IDs, ``Peer`` objects, - or even entire ``User``, ``Chat`` and ``Channel`` objects and even phone - numbers from people you have in your contacts. + but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects, + or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even + phone numbers from people you have in your contacts. Getting entities **************** @@ -73,7 +73,7 @@ become possible. Every entity the library encounters (in any response to any call) will by default be cached in the ``.session`` file (an SQLite database), to avoid performing unnecessary API calls. If the entity cannot be found, additonal -calls like ``ResolveUsernameRequest`` or ``GetContactsRequest`` may be +calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be made to obtain the required information. @@ -90,14 +90,14 @@ Entities vs. Input Entities On top of the normal types, the API also make use of what they call their ``Input*`` versions of objects. The input version of an entity (e.g. -``InputPeerUser``, ``InputChat``, etc.) only contains the minimum +:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum information that's required from Telegram to be able to identify -who you're referring to: a ``Peer``'s **ID** and **hash**. +who you're referring to: a :tl:`Peer`'s **ID** and **hash**. This ID/hash pair is unique per user, so if you use the pair given by another user **or bot** it will **not** work. -To save *even more* bandwidth, the API also makes use of the ``Peer`` +To save *even more* bandwidth, the API also makes use of the :tl:`Peer` versions, which just have an ID. This serves to identify them, but peers alone are not enough to use them. You need to know their hash before you can "use them". @@ -106,8 +106,8 @@ As we just mentioned, API calls don't need to know the whole information about the entities, only their ID and hash. For this reason, another method, ``.get_input_entity()`` is available. This will always use the cache while possible, making zero API calls most of the time. When a request is made, -if you provided the full entity, e.g. an ``User``, the library will convert -it to the required ``InputPeer`` automatically for you. +if you provided the full entity, e.g. an :tl:`User`, the library will convert +it to the required :tl:`InputPeer` automatically for you. **You should always favour** ``.get_input_entity()`` **over** ``.get_entity()`` for this reason! Calling the latter will always make an API call to get @@ -125,5 +125,5 @@ library, the raw requests you make to the API are also able to call client(SendMessageRequest('username', 'hello')) The library will call the ``.resolve()`` method of the request, which will -resolve ``'username'`` with the appropriated ``InputPeer``. Don't worry if +resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if you don't get this yet, but remember some of the details here are important. diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index a22ad725..612547af 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -315,7 +315,7 @@ library alone (when invoking a request), it means that you can now use ``Peer`` types or even usernames where a ``InputPeer`` is required. The object now has access to the ``client``, so that it can fetch the right type if needed, or access the session database. Furthermore, you can -reuse requests that need "autocast" (e.g. you put ``User`` but ``InputPeer`` +reuse requests that need "autocast" (e.g. you put :tl:`User` but ``InputPeer`` was needed), since ``.resolve()`` is called when invoking. Before, it was only done on object construction. diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index a91665fb..a3c4774e 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -91,13 +91,13 @@ class _EventCommon(abc.ABC): def _get_entity(self, msg_id, entity_id, chat=None): """ - Helper function to call GetMessages on the give msg_id and + Helper function to call :tl:`GetMessages` on the give msg_id and return the input entity whose ID is the given entity ID. - If ``chat`` is present it must be an InputPeer. + If ``chat`` is present it must be an :tl:`InputPeer`. - Returns a tuple of (entity, input_peer) if it was found, or - a tuple of (None, None) if it couldn't be. + Returns a tuple of ``(entity, input_peer)`` if it was found, or + a tuple of ``(None, None)`` if it couldn't be. """ try: if isinstance(chat, types.InputPeerChannel): @@ -124,7 +124,7 @@ class _EventCommon(abc.ABC): @property def input_chat(self): """ - The (:obj:`InputPeer`) (group, megagroup or channel) on which + The (:tl:`InputPeer`) (group, megagroup or channel) on which the event occurred. This doesn't have the title or anything, but is useful if you don't need those to avoid further requests. @@ -156,7 +156,7 @@ class _EventCommon(abc.ABC): @property def chat(self): """ - The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which + The (:tl:`User` | :tl:`Chat` | :tl:`Channel`, optional) on which the event occurred. This property may make an API call the first time to get the most up to date version of the chat (mostly when the event doesn't belong to a channel), so keep that in mind. @@ -312,8 +312,8 @@ class NewMessage(_EventBuilder): Represents the event of a new message. Members: - message (:obj:`Message`): - This is the original ``Message`` object. + message (:tl:`Message`): + This is the original :tl:`Message` object. is_private (:obj:`bool`): True if the message was sent as a private message. @@ -406,7 +406,7 @@ class NewMessage(_EventBuilder): @property def input_sender(self): """ - This (:obj:`InputPeer`) is the input version of the user who + This (:tl:`InputPeer`) is the input version of the user who sent the message. Similarly to ``input_chat``, this doesn't have things like username or similar, but still useful in some cases. @@ -434,7 +434,7 @@ class NewMessage(_EventBuilder): @property def sender(self): """ - This (:obj:`User`) may make an API call the first time to get + This (:tl:`User`) may make an API call the first time to get the most up to date version of the sender (mostly when the event doesn't belong to a channel), so keep that in mind. @@ -474,8 +474,8 @@ class NewMessage(_EventBuilder): @property def reply_message(self): """ - This (:obj:`Message`, optional) will make an API call the first - time to get the full ``Message`` object that one was replying to, + This optional :tl:`Message` will make an API call the first + time to get the full :tl:`Message` object that one was replying to, so use with care as there is no caching besides local caching yet. """ if not self.message.reply_to_msg_id: @@ -498,14 +498,14 @@ class NewMessage(_EventBuilder): @property def forward(self): """ - The unmodified (:obj:`MessageFwdHeader`, optional). + The unmodified :tl:`MessageFwdHeader`, if present.. """ return self.message.fwd_from @property def media(self): """ - The unmodified (:obj:`MessageMedia`, optional). + The unmodified :tl:`MessageMedia`, if present. """ return self.message.media @@ -513,7 +513,7 @@ class NewMessage(_EventBuilder): def photo(self): """ If the message media is a photo, - this returns the (:obj:`Photo`) object. + this returns the :tl:`Photo` object. """ if isinstance(self.message.media, types.MessageMediaPhoto): photo = self.message.media.photo @@ -524,7 +524,7 @@ class NewMessage(_EventBuilder): def document(self): """ If the message media is a document, - this returns the (:obj:`Document`) object. + this returns the :tl:`Document` object. """ if isinstance(self.message.media, types.MessageMediaDocument): doc = self.message.media.document @@ -547,7 +547,7 @@ class NewMessage(_EventBuilder): def audio(self): """ If the message media is a document with an Audio attribute, - this returns the (:obj:`Document`) object. + this returns the :tl:`Document` object. """ return self._document_by_attribute(types.DocumentAttributeAudio, lambda attr: not attr.voice) @@ -556,7 +556,7 @@ class NewMessage(_EventBuilder): def voice(self): """ If the message media is a document with a Voice attribute, - this returns the (:obj:`Document`) object. + this returns the :tl:`Document` object. """ return self._document_by_attribute(types.DocumentAttributeAudio, lambda attr: attr.voice) @@ -565,7 +565,7 @@ class NewMessage(_EventBuilder): def video(self): """ If the message media is a document with a Video attribute, - this returns the (:obj:`Document`) object. + this returns the :tl:`Document` object. """ return self._document_by_attribute(types.DocumentAttributeVideo) @@ -573,7 +573,7 @@ class NewMessage(_EventBuilder): def video_note(self): """ If the message media is a document with a Video attribute, - this returns the (:obj:`Document`) object. + this returns the :tl:`Document` object. """ return self._document_by_attribute(types.DocumentAttributeVideo, lambda attr: attr.round_message) @@ -582,7 +582,7 @@ class NewMessage(_EventBuilder): def gif(self): """ If the message media is a document with an Animated attribute, - this returns the (:obj:`Document`) object. + this returns the :tl:`Document` object. """ return self._document_by_attribute(types.DocumentAttributeAnimated) @@ -590,7 +590,7 @@ class NewMessage(_EventBuilder): def sticker(self): """ If the message media is a document with a Sticker attribute, - this returns the (:obj:`Document`) object. + this returns the :tl:`Document` object. """ return self._document_by_attribute(types.DocumentAttributeSticker) @@ -689,7 +689,7 @@ class ChatAction(_EventBuilder): new_photo (:obj:`bool`): ``True`` if there's a new chat photo (or it was removed). - photo (:obj:`Photo`, optional): + photo (:tl:`Photo`, optional): The new photo (or ``None`` if it was removed). @@ -793,7 +793,7 @@ class ChatAction(_EventBuilder): @property def pinned_message(self): """ - If ``new_pin`` is ``True``, this returns the (:obj:`Message`) + If ``new_pin`` is ``True``, this returns the (:tl:`Message`) object that was pinned. """ if self._pinned_message == 0: @@ -857,7 +857,7 @@ class ChatAction(_EventBuilder): @property def input_user(self): """ - Input version of the self.user property. + Input version of the ``self.user`` property. """ if self.input_users: return self._input_users[0] @@ -894,7 +894,7 @@ class ChatAction(_EventBuilder): @property def input_users(self): """ - Input version of the self.users property. + Input version of the ``self.users`` property. """ if self._input_users is None and self._user_peers: self._input_users = [] @@ -947,7 +947,7 @@ class UserUpdate(_EventBuilder): recently (:obj:`bool`): ``True`` if the user was seen within a day. - action (:obj:`SendMessageAction`, optional): + action (:tl:`SendMessageAction`, optional): The "typing" action if any the user is performing if any. cancel (:obj:`bool`): diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 532a8da7..8206fcaa 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -24,13 +24,15 @@ __log__ = logging.getLogger(__name__) class MtProtoSender: - """MTProto Mobile Protocol sender - (https://core.telegram.org/mtproto/description). + """ + MTProto Mobile Protocol sender + (https://core.telegram.org/mtproto/description). - Note that this class is not thread-safe, and calling send/receive - from two or more threads at the same time is undefined behaviour. - Rationale: a new connection should be spawned to send/receive requests - in parallel, so thread-safety (hence locking) isn't needed. + Note that this class is not thread-safe, and calling send/receive + from two or more threads at the same time is undefined behaviour. + Rationale: + a new connection should be spawned to send/receive requests + in parallel, so thread-safety (hence locking) isn't needed. """ def __init__(self, session, connection): diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 6a3822d5..046838bd 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -213,7 +213,7 @@ class TelegramClient(TelegramBareClient): Whether to force sending as SMS. Returns: - Information about the result of the request. + An instance of :tl:`SentCode`. """ phone = utils.parse_phone(phone) or self._phone phone_hash = self._phone_code_hash.get(phone) @@ -257,8 +257,9 @@ class TelegramClient(TelegramBareClient): This is only required if it is enabled in your account. bot_token (:obj:`str`): - Bot Token obtained by @BotFather to log in as a bot. - Cannot be specified with `phone` (only one of either allowed). + Bot Token obtained by `@BotFather `_ + to log in as a bot. Cannot be specified with ``phone`` (only + one of either allowed). force_sms (:obj:`bool`, optional): Whether to force sending the code request as SMS. @@ -276,8 +277,8 @@ class TelegramClient(TelegramBareClient): Similar to the first name, but for the last. Optional. Returns: - :obj:`TelegramClient`: - This client, so initialization can be chained with `.start()`. + This :obj:`TelegramClient`, so initialization + can be chained with ``.start()``. """ if code_callback is None: @@ -453,7 +454,7 @@ class TelegramClient(TelegramBareClient): Optional last name. Returns: - The new created user. + The new created :tl:`User`. """ if self.is_user_authorized(): self._check_events_pending_resolve() @@ -478,7 +479,7 @@ class TelegramClient(TelegramBareClient): Logs out Telegram and deletes the current ``*.session`` file. Returns: - True if the operation was successful. + ``True`` if the operation was successful. """ try: self(LogOutRequest()) @@ -496,12 +497,12 @@ class TelegramClient(TelegramBareClient): Args: input_peer (:obj:`bool`, optional): - Whether to return the ``InputPeerUser`` version or the normal - ``User``. This can be useful if you just need to know the ID + Whether to return the :tl:`InputPeerUser` version or the normal + :tl:`User`. This can be useful if you just need to know the ID of yourself. Returns: - :obj:`User`: Your own user. + Your own :tl:`User`. """ if input_peer and self._self_input_peer: return self._self_input_peer @@ -541,7 +542,7 @@ class TelegramClient(TelegramBareClient): offset_id (:obj:`int`, optional): The message ID to be used as an offset. - offset_peer (:obj:`InputPeer`, optional): + offset_peer (:tl:`InputPeer`, optional): The peer to be used as an offset. _total (:obj:`list`, optional): @@ -712,10 +713,10 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): To who will it be sent. - message (:obj:`str` | :obj:`Message`): + message (:obj:`str` | :tl:`Message`): The message to be sent, or another message object to resend. - reply_to (:obj:`int` | :obj:`Message`, optional): + reply_to (:obj:`int` | :tl:`Message`, optional): Whether to reply to a message or not. If an integer is provided, it should be the ID of the message that it should reply to. @@ -740,7 +741,7 @@ class TelegramClient(TelegramBareClient): Has no effect when sending a file. Returns: - the sent message. + The sent :tl:`Message`. """ if file is not None: return self.send_file( @@ -806,7 +807,7 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): To which entity the message(s) will be forwarded. - messages (:obj:`list` | :obj:`int` | :obj:`Message`): + messages (:obj:`list` | :obj:`int` | :tl:`Message`): The message(s) to forward, or their integer IDs. from_peer (:obj:`entity`): @@ -815,7 +816,7 @@ class TelegramClient(TelegramBareClient): order for the forward to work. Returns: - The forwarded messages. + The list of forwarded :tl:`Message`. """ if not utils.is_list_like(messages): messages = (messages,) @@ -882,7 +883,7 @@ class TelegramClient(TelegramBareClient): not modified at all. Returns: - the edited message + The edited :tl:`Message`. """ message, msg_entities = self._parse_message_text(message, parse_mode) request = EditMessageRequest( @@ -905,7 +906,7 @@ class TelegramClient(TelegramBareClient): be ``None`` for normal chats, but **must** be present for channels and megagroups. - message_ids (:obj:`list` | :obj:`int` | :obj:`Message`): + message_ids (:obj:`list` | :obj:`int` | :tl:`Message`): The IDs (or ID) or messages to be deleted. revoke (:obj:`bool`, optional): @@ -915,7 +916,7 @@ class TelegramClient(TelegramBareClient): This has no effect on channels or megagroups. Returns: - The affected messages. + The :tl:`AffectedMessages`. """ if not utils.is_list_like(message_ids): message_ids = (message_ids,) @@ -978,7 +979,7 @@ class TelegramClient(TelegramBareClient): you are still free to do so. wait_time (:obj:`int`): - Wait time between different ``GetHistoryRequest``. Use this + Wait time between different :tl:`GetHistoryRequest`. Use this parameter to avoid hitting the ``FloodWaitError`` as needed. If left to ``None``, it will default to 1 second only if the limit is higher than 3000. @@ -987,7 +988,7 @@ class TelegramClient(TelegramBareClient): A single-item list to pass the total parameter by reference. Yields: - Instances of ``telethon.tl.types.Message`` with extra attributes: + Instances of :tl:`Message` with extra attributes: * ``.sender`` = entity of the sender. * ``.fwd_from.sender`` = if fwd_from, who sent it originally. @@ -995,7 +996,7 @@ class TelegramClient(TelegramBareClient): * ``.to`` = entity to which the message was sent. Notes: - Telegram's flood wait limit for ``GetHistoryRequest`` seems to + Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to be around 30 seconds per 3000 messages, therefore a sleep of 1 second is the default for this limit (or above). You may need an higher limit, so you're free to set the ``batch_size`` that @@ -1101,7 +1102,7 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): The chat where these messages are located. - message (:obj:`list` | :obj:`Message`): + message (:obj:`list` | :tl:`Message`): Either a list of messages or a single message. max_id (:obj:`int`): @@ -1172,9 +1173,8 @@ class TelegramClient(TelegramBareClient): search (:obj:`str`, optional): Look for participants with this string in name/username. - filter (:obj:`ChannelParticipantsFilter`, optional): - The filter to be used, if you want e.g. only admins. See - https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html. + filter (:tl:`ChannelParticipantsFilter`, optional): + The filter to be used, if you want e.g. only admins Note that you might not have permissions for some filter. This has no effect for normal chats or users. @@ -1192,10 +1192,10 @@ class TelegramClient(TelegramBareClient): A single-item list to pass the total parameter by reference. Yields: - The ``User`` objects returned by ``GetParticipantsRequest`` + The :tl:`User` objects returned by :tl:`GetParticipantsRequest` with an additional ``.participant`` attribute which is the - matched ``ChannelParticipant`` type for channels/megagroups - or ``ChatParticipants`` for normal chats. + matched :tl:`ChannelParticipant` type for channels/megagroups + or :tl:`ChatParticipants` for normal chats. """ if isinstance(filter, type): filter = filter() @@ -1362,12 +1362,12 @@ class TelegramClient(TelegramBareClient): A callback function accepting two parameters: ``(sent bytes, total)``. - reply_to (:obj:`int` | :obj:`Message`): + reply_to (:obj:`int` | :tl:`Message`): Same as reply_to from .send_message(). attributes (:obj:`list`, optional): Optional attributes that override the inferred ones, like - ``DocumentAttributeFilename`` and so on. + :tl:`DocumentAttributeFilename` and so on. thumb (:obj:`str` | :obj:`bytes` | :obj:`file`, optional): Optional thumbnail (for videos). @@ -1390,7 +1390,7 @@ class TelegramClient(TelegramBareClient): it will be used to determine metadata from audio and video files. Returns: - The message (or messages) containing the sent file. + The :tl:`Message` (or messages) containing the sent file. """ # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. @@ -1551,7 +1551,7 @@ class TelegramClient(TelegramBareClient): return msg def send_voice_note(self, *args, **kwargs): - """Wrapper method around .send_file() with is_voice_note=True""" + """Wrapper method around :meth:`send_file` with is_voice_note=True.""" kwargs['is_voice_note'] = True return self.send_file(*args, **kwargs) @@ -1652,8 +1652,8 @@ class TelegramClient(TelegramBareClient): ``(sent bytes, total)``. Returns: - ``InputFileBig`` if the file size is larger than 10MB, - ``InputSizedFile`` (subclass of ``InputFile``) otherwise. + :tl:`InputFileBig` if the file size is larger than 10MB, + ``InputSizedFile`` (subclass of :tl:`InputFile`) otherwise. """ if isinstance(file, (InputFile, InputFileBig)): return file # Already uploaded @@ -1836,7 +1836,7 @@ class TelegramClient(TelegramBareClient): """ Downloads the given media, or the media from a specified Message. - message (:obj:`Message` | :obj:`Media`): + message (:tl:`Message` | :tl:`Media`): The media or message containing the media that will be downloaded. file (:obj:`str` | :obj:`file`, optional): @@ -1845,7 +1845,7 @@ class TelegramClient(TelegramBareClient): progress_callback (:obj:`callable`, optional): A callback function accepting two parameters: - ``(recv bytes, total)``. + ``(received bytes, total)``. Returns: ``None`` if no media was provided, or if it was Empty. On success @@ -2065,7 +2065,7 @@ class TelegramClient(TelegramBareClient): Downloads the given input location to a file. Args: - input_location (:obj:`InputFileLocation`): + input_location (:tl:`InputFileLocation`): The file location from which the file will be downloaded. file (:obj:`str` | :obj:`file`): @@ -2293,7 +2293,7 @@ class TelegramClient(TelegramBareClient): """ Turns the given entity into a valid Telegram user or chat. - entity (:obj:`str` | :obj:`int` | :obj:`Peer` | :obj:`InputPeer`): + entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`): The entity (or iterable of entities) to be transformed. If it's a string which can be converted to an integer or starts with '+' it will be resolved as if it were a phone number. @@ -2309,7 +2309,7 @@ class TelegramClient(TelegramBareClient): error will be raised. Returns: - ``User``, ``Chat`` or ``Channel`` corresponding to the input + :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the input entity. """ if utils.is_list_like(entity): @@ -2410,9 +2410,9 @@ class TelegramClient(TelegramBareClient): use this kind of InputUser, InputChat and so on, so this is the most suitable call to make for those cases. - entity (:obj:`str` | :obj:`int` | :obj:`Peer` | :obj:`InputPeer`): + entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`): The integer ID of an user or otherwise either of a - ``PeerUser``, ``PeerChat`` or ``PeerChannel``, for + :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for which to get its ``Input*`` version. If this ``Peer`` hasn't been seen before by the library, the top @@ -2423,7 +2423,7 @@ class TelegramClient(TelegramBareClient): a ValueError will be raised. Returns: - ``InputPeerUser``, ``InputPeerChat`` or ``InputPeerChannel``. + :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`. """ try: # First try to get the entity from cache, otherwise figure it out diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index a2b1a966..86265140 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -10,13 +10,13 @@ class Dialog: return instances of this class when calling :meth:`.get_dialogs()`. Args: - dialog (:obj:`Dialog`): + dialog (:tl:`Dialog`): The original ``Dialog`` instance. pinned (:obj:`bool`): Whether this dialog is pinned to the top or not. - message (:obj:`Message`): + message (:tl:`Message`): The last message sent on this dialog. Note that this member will not be updated when new messages arrive, it's only set on creation of the instance. @@ -27,7 +27,7 @@ class Dialog: entity (:obj:`entity`): The entity that belongs to this dialog (user, chat or channel). - input_entity (:obj:`InputPeer`): + input_entity (:tl:`InputPeer`): Input version of the entity. id (:obj:`int`): diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index fc40c1cf..f52ac6c9 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -128,7 +128,7 @@ class Draft: def send(self, clear=True, parse_mode='md'): """ Sends the contents of this draft to the dialog. This is just a - wrapper around send_message(dialog.input_entity, *args, **kwargs). + wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``. """ self._client.send_message(self._peer, self.text, reply_to=self.reply_to_msg_id, diff --git a/telethon/update_state.py b/telethon/update_state.py index 9f26e3a4..509697a0 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -11,8 +11,9 @@ __log__ = logging.getLogger(__name__) class UpdateState: - """Used to hold the current state of processed updates. - To retrieve an update, .poll() should be called. + """ + Used to hold the current state of processed updates. + To retrieve an update, :meth:`poll` should be called. """ WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers diff --git a/telethon/utils.py b/telethon/utils.py index 286853ad..faa1537a 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -38,8 +38,8 @@ VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$') def get_display_name(entity): """ - Gets the display name for the given entity, if it's an ``User``, - ``Chat`` or ``Channel``. Returns an empty string otherwise. + Gets the display name for the given entity, if it's an :tl:`User`, + :tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise. """ if isinstance(entity, User): if entity.last_name and entity.first_name: @@ -58,7 +58,7 @@ def get_display_name(entity): def get_extension(media): - """Gets the corresponding extension for any Telegram media""" + """Gets the corresponding extension for any Telegram media.""" # Photos are always compressed as .jpg by Telegram if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)): @@ -83,8 +83,10 @@ def _raise_cast_fail(entity, target): def get_input_peer(entity, allow_self=True): - """Gets the input peer for the given "entity" (user, chat or channel). - A TypeError is raised if the given entity isn't a supported type.""" + """ + Gets the input peer for the given "entity" (user, chat or channel). + A ``TypeError`` is raised if the given entity isn't a supported type. + """ try: if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') return entity @@ -129,7 +131,7 @@ def get_input_peer(entity, allow_self=True): def get_input_channel(entity): - """Similar to get_input_peer, but for InputChannel's alone""" + """Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone.""" try: if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') return entity @@ -146,7 +148,7 @@ def get_input_channel(entity): def get_input_user(entity): - """Similar to get_input_peer, but for InputUser's alone""" + """Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone.""" try: if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'): return entity @@ -175,7 +177,7 @@ def get_input_user(entity): def get_input_document(document): - """Similar to get_input_peer, but for documents""" + """Similar to :meth:`get_input_peer`, but for documents""" try: if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'): return document @@ -198,7 +200,7 @@ def get_input_document(document): def get_input_photo(photo): - """Similar to get_input_peer, but for documents""" + """Similar to :meth:`get_input_peer`, but for photos""" try: if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'): return photo @@ -218,7 +220,7 @@ def get_input_photo(photo): def get_input_geo(geo): - """Similar to get_input_peer, but for geo points""" + """Similar to :meth:`get_input_peer`, but for geo points""" try: if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'): return geo @@ -241,10 +243,11 @@ def get_input_geo(geo): def get_input_media(media, is_photo=False): - """Similar to get_input_peer, but for media. + """ + Similar to :meth:`get_input_peer`, but for media. - If the media is a file location and is_photo is known to be True, - it will be treated as an InputMediaUploadedPhoto. + If the media is a file location and ``is_photo`` is known to be ``True``, + it will be treated as an :tl:`InputMediaUploadedPhoto`. """ try: if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia'): @@ -317,7 +320,7 @@ def get_input_media(media, is_photo=False): def is_image(file): """ - Returns True if the file extension looks like an image file to Telegram. + Returns ``True`` if the file extension looks like an image file to Telegram. """ if not isinstance(file, str): return False @@ -326,23 +329,23 @@ def is_image(file): def is_audio(file): - """Returns True if the file extension looks like an audio file""" + """Returns ``True`` if the file extension looks like an audio file.""" return (isinstance(file, str) and (mimetypes.guess_type(file)[0] or '').startswith('audio/')) def is_video(file): - """Returns True if the file extension looks like a video file""" + """Returns ``True`` if the file extension looks like a video file.""" return (isinstance(file, str) and (mimetypes.guess_type(file)[0] or '').startswith('video/')) def is_list_like(obj): """ - Returns True if the given object looks like a list. + Returns ``True`` if the given object looks like a list. - Checking if hasattr(obj, '__iter__') and ignoring str/bytes is not - enough. Things like open() are also iterable (and probably many + Checking ``if hasattr(obj, '__iter__')`` and ignoring ``str/bytes`` is not + enough. Things like ``open()`` are also iterable (and probably many other things), so just support the commonly known list-like objects. """ return isinstance(obj, (list, tuple, set, dict, @@ -350,7 +353,7 @@ def is_list_like(obj): def parse_phone(phone): - """Parses the given phone, or returns None if it's invalid""" + """Parses the given phone, or returns ``None`` if it's invalid.""" if isinstance(phone, int): return str(phone) else: @@ -365,7 +368,7 @@ def parse_username(username): both the stripped, lowercase username and whether it is a joinchat/ hash (in which case is not lowercase'd). - Returns None if the username is not valid. + Returns ``None`` if the ``username`` is not valid. """ username = username.strip() m = USERNAME_RE.match(username) @@ -386,7 +389,7 @@ def parse_username(username): def _fix_peer_id(peer_id): """ Fixes the peer ID for chats and channels, in case the users - mix marking the ID with the ``Peer()`` constructors. + mix marking the ID with the :tl:`Peer` constructors. """ peer_id = abs(peer_id) if str(peer_id).startswith('100'): @@ -401,7 +404,7 @@ def get_peer_id(peer): chat ID is negated, and channel ID is prefixed with -100. The original ID and the peer type class can be returned with - a call to utils.resolve_id(marked_id). + a call to :meth:`resolve_id(marked_id)`. """ # First we assert it's a Peer TLObject, or early return for integers if isinstance(peer, int): @@ -450,7 +453,7 @@ def get_peer_id(peer): def resolve_id(marked_id): - """Given a marked ID, returns the original ID and its Peer type""" + """Given a marked ID, returns the original ID and its :tl:`Peer` type.""" if marked_id >= 0: return marked_id, PeerUser @@ -461,8 +464,10 @@ def resolve_id(marked_id): def get_appropriated_part_size(file_size): - """Gets the appropriated part size when uploading or downloading files, - given an initial file size""" + """ + Gets the appropriated part size when uploading or downloading files, + given an initial file size. + """ if file_size <= 104857600: # 100MB return 128 if file_size <= 786432000: # 750MB From 898e550335b9979a131dd956c8aa26a01d341b4c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Mar 2018 12:09:33 +0100 Subject: [PATCH 340/361] Except the right type for get_input_peer (closes #722) --- telethon/sessions/memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index e5223cac..5faf721d 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -78,7 +78,7 @@ class MemorySession(Session): try: p = utils.get_input_peer(e, allow_self=False) marked_id = utils.get_peer_id(p) - except ValueError: + except TypeError: return if isinstance(p, (InputPeerUser, InputPeerChannel)): From 69d283a29648cbea78640c66a6b537774f0a0a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20R=C3=A4sener?= Date: Sat, 24 Mar 2018 12:12:47 +0100 Subject: [PATCH 341/361] Tests cleanup (#717) --- telethon_tests/parser_test.py | 5 - .../{crypto_test.py => test_crypto.py} | 100 ++++++++++-------- ...her_level_test.py => test_higher_level.py} | 17 +-- .../{network_test.py => test_network.py} | 15 +-- telethon_tests/test_parser.py | 8 ++ telethon_tests/test_tl.py | 8 ++ .../{utils_test.py => test_utils.py} | 37 ++++--- telethon_tests/tl_test.py | 5 - 8 files changed, 107 insertions(+), 88 deletions(-) delete mode 100644 telethon_tests/parser_test.py rename telethon_tests/{crypto_test.py => test_crypto.py} (57%) rename telethon_tests/{higher_level_test.py => test_higher_level.py} (71%) rename telethon_tests/{network_test.py => test_network.py} (72%) create mode 100644 telethon_tests/test_parser.py create mode 100644 telethon_tests/test_tl.py rename telethon_tests/{utils_test.py => test_utils.py} (52%) delete mode 100644 telethon_tests/tl_test.py diff --git a/telethon_tests/parser_test.py b/telethon_tests/parser_test.py deleted file mode 100644 index fc366b45..00000000 --- a/telethon_tests/parser_test.py +++ /dev/null @@ -1,5 +0,0 @@ -import unittest - - -class ParserTests(unittest.TestCase): - """There are no tests yet""" diff --git a/telethon_tests/crypto_test.py b/telethon_tests/test_crypto.py similarity index 57% rename from telethon_tests/crypto_test.py rename to telethon_tests/test_crypto.py index 17453f62..136e6091 100644 --- a/telethon_tests/crypto_test.py +++ b/telethon_tests/test_crypto.py @@ -3,8 +3,7 @@ from hashlib import sha1 import telethon.helpers as utils from telethon.crypto import AES, Factorization -from telethon.crypto import rsa -from Crypto.PublicKey import RSA as PyCryptoRSA +# from crypto.PublicKey import RSA as PyCryptoRSA class CryptoTests(unittest.TestCase): @@ -22,37 +21,38 @@ class CryptoTests(unittest.TestCase): self.cipher_text_padded = b"W\xd1\xed'\x01\xa6c\xc3\xcb\xef\xaa\xe5\x1d\x1a" \ b"[\x1b\xdf\xcdI\x1f>Z\n\t\xb9\xd2=\xbaF\xd1\x8e'" - @staticmethod - def test_sha1(): + def test_sha1(self): string = 'Example string' hash_sum = sha1(string.encode('utf-8')).digest() expected = b'\nT\x92|\x8d\x06:)\x99\x04\x8e\xf8j?\xc4\x8e\xd3}m9' - assert hash_sum == expected, 'Invalid sha1 hash_sum representation (should be {}, but is {})'\ - .format(expected, hash_sum) + self.assertEqual(hash_sum, expected, + msg='Invalid sha1 hash_sum representation (should be {}, but is {})' + .format(expected, hash_sum)) + @unittest.skip("test_aes_encrypt needs fix") def test_aes_encrypt(self): value = AES.encrypt_ige(self.plain_text, self.key, self.iv) take = 16 # Don't take all the bytes, since latest involve are random padding - assert value[:take] == self.cipher_text[:take],\ - ('Ciphered text ("{}") does not equal expected ("{}")' - .format(value[:take], self.cipher_text[:take])) + self.assertEqual(value[:take], self.cipher_text[:take], + msg='Ciphered text ("{}") does not equal expected ("{}")' + .format(value[:take], self.cipher_text[:take])) value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv) - assert value == self.cipher_text_padded, ( - 'Ciphered text ("{}") does not equal expected ("{}")' - .format(value, self.cipher_text_padded)) + self.assertEqual(value, self.cipher_text_padded, + msg='Ciphered text ("{}") does not equal expected ("{}")' + .format(value, self.cipher_text_padded)) def test_aes_decrypt(self): # The ciphered text must always be padded value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv) - assert value == self.plain_text_padded, ( - 'Decrypted text ("{}") does not equal expected ("{}")' - .format(value, self.plain_text_padded)) + self.assertEqual(value, self.plain_text_padded, + msg='Decrypted text ("{}") does not equal expected ("{}")' + .format(value, self.plain_text_padded)) - @staticmethod - def test_calc_key(): + @unittest.skip("test_calc_key needs fix") + def test_calc_key(self): # TODO Upgrade test for MtProto 2.0 shared_key = b'\xbc\xd2m\xb7\xcav\xf4][\x88\x83\' \xf3\x11\x8as\xd04\x941\xae' \ b'*O\x03\x86\x9a/H#\x1a\x8c\xb5j\xe9$\xe0IvCm^\xe70\x1a5C\t\x16' \ @@ -78,10 +78,12 @@ class CryptoTests(unittest.TestCase): b'\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1' \ b'\xa7\xa0\xf7\x0f' - assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format( - expected_key, key) - assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format( - expected_iv, iv) + self.assertEqual(key, expected_key, + msg='Invalid key (expected ("{}"), got ("{}"))' + .format(expected_key, key)) + self.assertEqual(iv, expected_iv, + msg='Invalid IV (expected ("{}"), got ("{}"))' + .format(expected_iv, iv)) # Calculate key being the server msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]' @@ -94,13 +96,14 @@ class CryptoTests(unittest.TestCase): expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd' \ b'\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc' - assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format( - expected_key, key) - assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format( - expected_iv, iv) + self.assertEqual(key, expected_key, + msg='Invalid key (expected ("{}"), got ("{}"))' + .format(expected_key, key)) + self.assertEqual(iv, expected_iv, + msg='Invalid IV (expected ("{}"), got ("{}"))' + .format(expected_iv, iv)) - @staticmethod - def test_generate_key_data_from_nonce(): + def test_generate_key_data_from_nonce(self): 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') @@ -108,30 +111,33 @@ class CryptoTests(unittest.TestCase): 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, 'IV ("{}") does not equal expected ("{}")'.format( - iv, expected_iv) + self.assertEqual(key, expected_key, + msg='Key ("{}") does not equal expected ("{}")' + .format(key, expected_key)) + self.assertEqual(iv, expected_iv, + msg='IV ("{}") does not equal expected ("{}")' + .format(iv, expected_iv)) - @staticmethod - def test_fingerprint_from_key(): - assert rsa._compute_fingerprint(PyCryptoRSA.importKey( - '-----BEGIN RSA PUBLIC KEY-----\n' - 'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n' - 'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n' - 'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n' - 'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n' - '8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n' - 'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n' - '-----END RSA PUBLIC KEY-----' - )) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated' + # test_fringerprint_from_key can't be skipped due to ImportError + # def test_fingerprint_from_key(self): + # assert rsa._compute_fingerprint(PyCryptoRSA.importKey( + # '-----BEGIN RSA PUBLIC KEY-----\n' + # 'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n' + # 'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n' + # 'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n' + # 'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n' + # '8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n' + # 'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n' + # '-----END RSA PUBLIC KEY-----' + # )) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated' - @staticmethod - def test_factorize(): + def test_factorize(self): pq = 3118979781119966969 p, q = Factorization.factorize(pq) if p > q: p, q = q, p - assert p == 1719614201, 'Factorized pair did not yield the correct result' - assert q == 1813767169, 'Factorized pair did not yield the correct result' + self.assertEqual(p, 1719614201, + msg='Factorized pair did not yield the correct result') + self.assertEqual(q, 1813767169, + msg='Factorized pair did not yield the correct result') diff --git a/telethon_tests/higher_level_test.py b/telethon_tests/test_higher_level.py similarity index 71% rename from telethon_tests/higher_level_test.py rename to telethon_tests/test_higher_level.py index 7433fac9..8e933056 100644 --- a/telethon_tests/higher_level_test.py +++ b/telethon_tests/test_higher_level.py @@ -10,16 +10,17 @@ from telethon import TelegramClient api_id = None api_hash = None -if not api_id or not api_hash: - raise ValueError('Please fill in both your api_id and api_hash.') - class HigherLevelTests(unittest.TestCase): - @staticmethod - def test_cdn_download(): + def setUp(self): + if not api_id or not api_hash: + raise ValueError('Please fill in both your api_id and api_hash.') + + @unittest.skip("you can't seriously trash random mobile numbers like that :)") + def test_cdn_download(self): client = TelegramClient(None, api_id, api_hash) client.session.set_dc(0, '149.154.167.40', 80) - assert client.connect() + self.assertTrue(client.connect()) try: phone = '+999662' + str(randint(0, 9999)).zfill(4) @@ -37,11 +38,11 @@ class HigherLevelTests(unittest.TestCase): out = BytesIO() client.download_media(msg, out) - assert sha256(data).digest() == sha256(out.getvalue()).digest() + self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest()) out = BytesIO() client.download_media(msg, out) # Won't redirect - assert sha256(data).digest() == sha256(out.getvalue()).digest() + self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest()) client.log_out() finally: diff --git a/telethon_tests/network_test.py b/telethon_tests/test_network.py similarity index 72% rename from telethon_tests/network_test.py rename to telethon_tests/test_network.py index 559eab45..031ad99d 100644 --- a/telethon_tests/network_test.py +++ b/telethon_tests/test_network.py @@ -23,8 +23,9 @@ def run_server_echo_thread(port): class NetworkTests(unittest.TestCase): - @staticmethod - def test_tcp_client(): + + @unittest.skip("test_tcp_client needs fix") + def test_tcp_client(self): port = random.randint(50000, 60000) # Arbitrary non-privileged port run_server_echo_thread(port) @@ -32,12 +33,12 @@ class NetworkTests(unittest.TestCase): client = TcpClient() client.connect('localhost', port) client.write(msg) - assert msg == client.read( - 15), 'Read message does not equal sent message' + self.assertEqual(msg, client.read(15), + msg='Read message does not equal sent message') client.close() - @staticmethod - def test_authenticator(): + @unittest.skip("Some parameters changed, so IP doesn't go there anymore.") + def test_authenticator(self): transport = Connection('149.154.167.91', 443) - authenticator.do_authentication(transport) + self.assertTrue(authenticator.do_authentication(transport)) transport.close() diff --git a/telethon_tests/test_parser.py b/telethon_tests/test_parser.py new file mode 100644 index 00000000..c87686a6 --- /dev/null +++ b/telethon_tests/test_parser.py @@ -0,0 +1,8 @@ +import unittest + + +class ParserTests(unittest.TestCase): + """There are no tests yet""" + @unittest.skip("there should be parser tests") + def test_parser(self): + self.assertTrue(True) diff --git a/telethon_tests/test_tl.py b/telethon_tests/test_tl.py new file mode 100644 index 00000000..189259f5 --- /dev/null +++ b/telethon_tests/test_tl.py @@ -0,0 +1,8 @@ +import unittest + + +class TLTests(unittest.TestCase): + """There are no tests yet""" + @unittest.skip("there should be TL tests") + def test_tl(self): + self.assertTrue(True) \ No newline at end of file diff --git a/telethon_tests/utils_test.py b/telethon_tests/test_utils.py similarity index 52% rename from telethon_tests/utils_test.py rename to telethon_tests/test_utils.py index 790f3f4d..4a550e3d 100644 --- a/telethon_tests/utils_test.py +++ b/telethon_tests/test_utils.py @@ -5,8 +5,7 @@ from telethon.extensions import BinaryReader class UtilsTests(unittest.TestCase): - @staticmethod - def test_binary_writer_reader(): + def test_binary_writer_reader(self): # Test that we can read properly data = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x88A\x00\x00\x00\x00\x00\x009@\x1a\x1b\x1c\x1d\x1e\x1f ' \ @@ -15,29 +14,35 @@ class UtilsTests(unittest.TestCase): with BinaryReader(data) as reader: value = reader.read_byte() - assert value == 1, 'Example byte should be 1 but is {}'.format(value) + self.assertEqual(value, 1, + msg='Example byte should be 1 but is {}'.format(value)) value = reader.read_int() - assert value == 5, 'Example integer should be 5 but is {}'.format(value) + self.assertEqual(value, 5, + msg='Example integer should be 5 but is {}'.format(value)) value = reader.read_long() - assert value == 13, 'Example long integer should be 13 but is {}'.format(value) + self.assertEqual(value, 13, + msg='Example long integer should be 13 but is {}'.format(value)) value = reader.read_float() - assert value == 17.0, 'Example float should be 17.0 but is {}'.format(value) + self.assertEqual(value, 17.0, + msg='Example float should be 17.0 but is {}'.format(value)) value = reader.read_double() - assert value == 25.0, 'Example double should be 25.0 but is {}'.format(value) + self.assertEqual(value, 25.0, + msg='Example double should be 25.0 but is {}'.format(value)) value = reader.read(7) - assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \ - .format(bytes([26, 27, 28, 29, 30, 31, 32]), value) + self.assertEqual(value, bytes([26, 27, 28, 29, 30, 31, 32]), + msg='Example bytes should be {} but is {}' + .format(bytes([26, 27, 28, 29, 30, 31, 32]), value)) value = reader.read_large_int(128, signed=False) - assert value == 2**127, 'Example large integer should be {} but is {}'.format(2**127, value) + self.assertEqual(value, 2**127, + msg='Example large integer should be {} but is {}'.format(2**127, value)) - @staticmethod - def test_binary_tgwriter_tgreader(): + def test_binary_tgwriter_tgreader(self): small_data = os.urandom(33) small_data_padded = os.urandom(19) # +1 byte for length = 20 (%4 = 0) @@ -53,9 +58,9 @@ class UtilsTests(unittest.TestCase): # And then try reading it without errors (it should be unharmed!) for datum in data: value = reader.tgread_bytes() - assert value == datum, 'Example bytes should be {} but is {}'.format( - datum, value) + self.assertEqual(value, datum, + msg='Example bytes should be {} but is {}'.format(datum, value)) value = reader.tgread_string() - assert value == string, 'Example string should be {} but is {}'.format( - string, value) + self.assertEqual(value, string, + msg='Example string should be {} but is {}'.format(string, value)) diff --git a/telethon_tests/tl_test.py b/telethon_tests/tl_test.py deleted file mode 100644 index 37f0bbe5..00000000 --- a/telethon_tests/tl_test.py +++ /dev/null @@ -1,5 +0,0 @@ -import unittest - - -class TLTests(unittest.TestCase): - """There are no tests yet""" From 13e59983af49e2ec7dca23ee333b3d81b1b4fb00 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Mar 2018 12:41:42 +0100 Subject: [PATCH 342/361] Slightly change docs for events (#668) --- readthedocs/extra/basic/getting-started.rst | 20 ++++++++++++++++++++ telethon/events/__init__.py | 6 ++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index 87c142e9..e40bae44 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -66,6 +66,26 @@ Basic Usage **More details**: :ref:`telegram-client` +Handling Updates +**************** + + .. code-block:: python + + from telethon import events + + # We need to have some worker running + client.updates.workers = 1 + + @client.on(events.NewMessage(incoming=True, pattern='(?i)hi')) + def handler(event): + event.reply('Hello!') + + # If you want to handle updates you can't let the script end. + input('Press enter to exit.') + + **More details**: :ref:`working-with-updates` + + ---------- You can continue by clicking on the "More details" link below each diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index a3c4774e..8b80bfd8 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -37,8 +37,10 @@ class _EventBuilder(abc.ABC): only matching chats will be handled. blacklist_chats (:obj:`bool`, optional): - Whether to treat the the list of chats as a blacklist (if - it matches it will NOT be handled) or a whitelist (default). + Whether to treat the chats as a blacklist instead of + as a whitelist (default). This means that every chat + will be handled *except* those specified in ``chats`` + which will be ignored if ``blacklist_chats=True``. """ def __init__(self, chats=None, blacklist_chats=False): self.chats = chats From 790b0d2d23a7b827088cc21591f0c7dc71ae6262 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Mar 2018 18:34:07 +0100 Subject: [PATCH 343/361] Guess entity type on positive IDs in events and avoid some RPCs Now specifying a single positive integer ID will add all the types to the white/blacklist so it can be "guessed". Explicit peers will always be only that type, and an RPC is avoided (since it was not needed to begin with). --- telethon/events/__init__.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 8b80bfd8..ff8f8d48 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -20,10 +20,24 @@ def _into_id_set(client, chats): result = set() for chat in chats: - chat = client.get_input_entity(chat) - if isinstance(chat, types.InputPeerSelf): - chat = client.get_me(input_peer=True) - result.add(utils.get_peer_id(chat)) + if isinstance(chat, int): + if chat < 0: + result.add(chat) # Explicitly marked IDs are negative + else: + result.update({ # Support all valid types of peers + utils.get_peer_id(types.PeerUser(chat)), + utils.get_peer_id(types.PeerChat(chat)), + utils.get_peer_id(types.PeerChannel(chat)), + }) + elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: + # 0x2d45687 == crc32(b'Peer') + result.add(utils.get_peer_id(chat)) + else: + chat = client.get_input_entity(chat) + if isinstance(chat, types.InputPeerSelf): + chat = client.get_me(input_peer=True) + result.add(utils.get_peer_id(chat)) + return result From 1c9dc8dc63f59a6e7b385e5c9ebbd54eb67be73c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Mar 2018 18:44:13 +0100 Subject: [PATCH 344/361] Fix getting ID on custom Dialog class from dce0fd9 --- telethon/tl/custom/dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index 86265140..6b386163 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -59,7 +59,7 @@ class Dialog: self.entity = entities[utils.get_peer_id(dialog.peer)] self.input_entity = utils.get_input_peer(self.entity) - self.id = utils.get_peer_id(self.input_entity) + self.id = utils.get_peer_id(self.entity) # ^ May be InputPeerSelf() self.name = utils.get_display_name(self.entity) self.unread_count = dialog.unread_count From 08d71f69ee22c492f4a34a54ee5ae5aab3eedb46 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 27 Mar 2018 16:03:45 +0800 Subject: [PATCH 345/361] Fix author mismatch in the documentation (#726) --- readthedocs/conf.py | 2 +- readthedocs/extra/examples/chats-and-channels.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/readthedocs/conf.py b/readthedocs/conf.py index 35dadb24..27f1ae52 100644 --- a/readthedocs/conf.py +++ b/readthedocs/conf.py @@ -157,7 +157,7 @@ latex_elements = { # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Telethon.tex', 'Telethon Documentation', - 'Jeff', 'manual'), + author, 'manual'), ] diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index f38519c6..7836a348 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -147,8 +147,8 @@ __ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html __ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html __ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html __ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html -__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html __ https://github.com/LonamiWebs/Telethon/issues/573 +__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html Recent Actions From ae2abd2ba9b9373477341e888e773d5c3b076747 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 27 Mar 2018 11:00:46 +0200 Subject: [PATCH 346/361] Add __str__/.stringify() to tl.custom for completeness --- telethon/tl/custom/dialog.py | 17 +++++++++++++++++ telethon/tl/custom/draft.py | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index 6b386163..921a4c34 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -1,4 +1,5 @@ from . import Draft +from .. import TLObject from ... import utils @@ -73,3 +74,19 @@ class Dialog: ``client.send_message(dialog.input_entity, *args, **kwargs)``. """ return self._client.send_message(self.input_entity, *args, **kwargs) + + def to_dict(self): + return { + '_': 'Dialog', + 'name': self.name, + 'date': self.date, + 'draft': self.draft, + 'message': self.message, + 'entity': self.entity, + } + + def __str__(self): + return TLObject.pretty_format(self.to_dict()) + + def stringify(self): + return TLObject.pretty_format(self.to_dict(), indent=0) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index f52ac6c9..09c0e6ac 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -1,7 +1,9 @@ import datetime +from .. import TLObject from ..functions.messages import SaveDraftRequest from ..types import UpdateDraftMessage, DraftMessage +from ...errors import RPCError from ...extensions import markdown @@ -141,3 +143,24 @@ class Draft: Deletes this draft, and returns ``True`` on success. """ return self.set_message(text='') + + def to_dict(self): + try: + entity = self.entity + except RPCError as e: + entity = e + + return { + '_': 'Draft', + 'text': self.text, + 'entity': entity, + 'date': self.date, + 'link_preview': self.link_preview, + 'reply_to_msg_id': self.reply_to_msg_id + } + + def __str__(self): + return TLObject.pretty_format(self.to_dict()) + + def stringify(self): + return TLObject.pretty_format(self.to_dict(), indent=0) From 8d652c35a17cb698c7ff262f71095467454cbca2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 27 Mar 2018 11:22:31 +0200 Subject: [PATCH 347/361] Add missing Photo/Document cases to get_input_media --- telethon/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/telethon/utils.py b/telethon/utils.py index faa1537a..2427ce13 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -261,12 +261,22 @@ def get_input_media(media, is_photo=False): ttl_seconds=media.ttl_seconds ) + if isinstance(media, (Photo, photos.Photo, PhotoEmpty)): + return InputMediaPhoto( + id=get_input_photo(media) + ) + if isinstance(media, MessageMediaDocument): return InputMediaDocument( id=get_input_document(media.document), ttl_seconds=media.ttl_seconds ) + if isinstance(media, (Document, DocumentEmpty)): + return InputMediaDocument( + id=get_input_document(media) + ) + if isinstance(media, FileLocation): if is_photo: return InputMediaUploadedPhoto(file=media) From 7b94530bfcb283ba3c45e8fd7e6d63da509b8221 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 27 Mar 2018 11:29:47 +0200 Subject: [PATCH 348/361] Move me/self check on get_input_entity to the beginning It would otherwise fail since the addition of getting entity by exact name if someone had 'me' or 'self' as their name. --- telethon/telegram_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 046838bd..4eb33801 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -2425,6 +2425,9 @@ class TelegramClient(TelegramBareClient): Returns: :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`. """ + if peer in ('me', 'self'): + return InputPeerSelf() + try: # First try to get the entity from cache, otherwise figure it out return self.session.get_input_entity(peer) @@ -2432,8 +2435,6 @@ class TelegramClient(TelegramBareClient): pass if isinstance(peer, str): - if peer in ('me', 'self'): - return InputPeerSelf() return utils.get_input_peer(self._get_entity_from_string(peer)) original_peer = peer From 302a823c88918219c0a5f184f7f6a7f17c76e972 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 27 Mar 2018 17:15:22 +0200 Subject: [PATCH 349/361] Fix invalid access to .participants on chat forbidden result --- telethon/telegram_client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 4eb33801..7998d0ed 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -83,7 +83,7 @@ from .tl.types import ( InputMessageEntityMentionName, DocumentAttributeVideo, UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize, - PhotoSizeEmpty, MessageService + PhotoSizeEmpty, MessageService, ChatParticipants ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -1278,6 +1278,11 @@ class TelegramClient(TelegramBareClient): elif isinstance(entity, InputPeerChat): # TODO We *could* apply the `filter` here ourselves full = self(GetFullChatRequest(entity.chat_id)) + if not isinstance(full.full_chat.participants, ChatParticipants): + # ChatParticipantsForbidden won't have ``.participants`` + _total[0] = 0 + return + if _total: _total[0] = len(full.full_chat.participants.participants) From 6c9becb1ed5673faa142223175416203e8a878fe Mon Sep 17 00:00:00 2001 From: Kyle2142 <19349418+Kyle2142@users.noreply.github.com> Date: Tue, 27 Mar 2018 17:35:33 +0200 Subject: [PATCH 350/361] Add edit_2fa function (#725) --- telethon/telegram_client.py | 76 ++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 7998d0ed..9c649a0a 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -38,12 +38,12 @@ from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError, - PhoneNumberOccupiedError + PhoneNumberOccupiedError, EmailUnconfirmedError, PasswordEmptyError ) from .network import ConnectionMode from .tl.custom import Draft, Dialog from .tl.functions.account import ( - GetPasswordRequest + GetPasswordRequest, UpdatePasswordSettingsRequest ) from .tl.functions.auth import ( CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest, @@ -86,6 +86,7 @@ from .tl.types import ( PhotoSizeEmpty, MessageService, ChatParticipants ) from .tl.types.messages import DialogsSlice +from .tl.types.account import PasswordInputSettings, NoPassword from .extensions import markdown, html __log__ = logging.getLogger(__name__) @@ -2476,4 +2477,75 @@ class TelegramClient(TelegramBareClient): 'Make sure you have encountered this peer before.'.format(peer) ) + def edit_2fa(self, current_password=None, new_password=None, hint='', + email=None): + """ + Changes the 2FA settings of the logged in user, according to the + passed parameters. Take note of the parameter explanations. + + Has no effect if both current and new password are omitted. + + current_password (:obj:`str`, optional): + The current password, to authorize changing to ``new_password``. + Must be set if changing existing 2FA settings. + Must **not** be set if 2FA is currently disabled. + Passing this by itself will remove 2FA (if correct). + + new_password (:obj:`str`, optional): + The password to set as 2FA. + If 2FA was already enabled, ``current_password`` **must** be set. + Leaving this blank or ``None`` will remove the password. + + hint (:obj:`str`, optional): + Hint to be displayed by Telegram when it asks for 2FA. + Leaving unspecified is highly discouraged. + Has no effect if ``new_password`` is not set. + + email (:obj:`str`, optional): + Recovery and verification email. Raises ``EmailUnconfirmedError`` + if value differs from current one, and has no effect if + ``new_password`` is not set. + + Returns: + ``True`` if successful, ``False`` otherwise. + """ + if new_password is None and current_password is None: + return False + + pass_result = self(GetPasswordRequest()) + if isinstance(pass_result, NoPassword) and current_password: + current_password = None + + salt_random = os.urandom(8) + salt = pass_result.new_salt + salt_random + if not current_password: + current_password_hash = salt + else: + current_password = pass_result.current_salt +\ + current_password.encode() + pass_result.current_salt + current_password_hash = hashlib.sha256(current_password).digest() + + if new_password: # Setting new password + new_password = salt + new_password.encode('utf-8') + salt + new_password_hash = hashlib.sha256(new_password).digest() + new_settings = PasswordInputSettings( + new_salt=salt, + new_password_hash=new_password_hash, + hint=hint + ) + if email: # If enabling 2FA or changing email + new_settings.email = email # TG counts empty string as None + return self(UpdatePasswordSettingsRequest( + current_password_hash, new_settings=new_settings + )) + else: # Removing existing password + return self(UpdatePasswordSettingsRequest( + current_password_hash, + new_settings=PasswordInputSettings( + new_salt=bytes(), + new_password_hash=bytes(), + hint=hint + ) + )) + # endregion From 04695d365702e9adea20eb8f98db9dd6dcf70cff Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 27 Mar 2018 17:52:39 +0200 Subject: [PATCH 351/361] Update docs regarding 2FA and getting entities by ID --- readthedocs/extra/basic/creating-a-client.rst | 32 ++----------------- readthedocs/extra/basic/entities.rst | 23 ++++--------- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 384ebd47..ba4adc28 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -150,35 +150,9 @@ The mentioned ``.start()`` method will handle this for you as well, but you must set the ``password=`` parameter beforehand (it won't be asked). If you don't have 2FA enabled, but you would like to do so through the library, -take as example the following code snippet: - - .. code-block:: python - - import os - from hashlib import sha256 - from telethon.tl.functions import account - from telethon.tl.types.account import PasswordInputSettings - - new_salt = client(account.GetPasswordRequest()).new_salt - salt = new_salt + os.urandom(8) # new random salt - - pw = 'secret'.encode('utf-8') # type your new password here - hint = 'hint' - - pw_salted = salt + pw + salt - pw_hash = sha256(pw_salted).digest() - - result = client(account.UpdatePasswordSettingsRequest( - current_password_hash=salt, - new_settings=PasswordInputSettings( - new_salt=salt, - new_password_hash=pw_hash, - hint=hint - ) - )) - -Thanks to `Issue 259 `_ -for the tip! +use :obj:`client.edit_2fa ` +for it. Be sure to know what you're doing when using this function and you +won't run into any problems. __ https://github.com/Anorov/PySocks#installation diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index c7e55524..ab04a165 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -32,7 +32,7 @@ you're able to just do this: # Dialogs are the "conversations you have open". # This method returns a list of Dialog, which # has the .entity attribute and other information. - dialogs = client.get_dialogs(limit=200) + dialogs = client.get_dialogs() # All of these work and do the same. lonami = client.get_entity('lonami') @@ -44,27 +44,18 @@ you're able to just do this: contact = client.get_entity('+34xxxxxxxxx') friend = client.get_entity(friend_id) - # Using Peer/InputPeer (note that the API may return these) - # users, chats and channels may all have the same ID, so it's - # necessary to wrap (at least) chat and channels inside Peer. - # - # NOTICE how the IDs *must* be wrapped inside a Peer() so the - # library knows their type. + # Getting entities through their ID (User, Chat or Channel) + entity = client.get_entity(some_id) + + # You can be more explicit about the type for said ID by wrapping + # it inside a Peer instance. This is recommended but not necessary. from telethon.tl.types import PeerUser, PeerChat, PeerChannel + my_user = client.get_entity(PeerUser(some_id)) my_chat = client.get_entity(PeerChat(some_id)) my_channel = client.get_entity(PeerChannel(some_id)) -.. warning:: - - As it has been mentioned already, getting the entity of a channel - through e.g. ``client.get_entity(channel id)`` will **not** work. - You would use ``client.get_entity(types.PeerChannel(channel id))``. - Remember that supergroups are channels and normal groups are chats. - This is a common mistake! - - All methods in the :ref:`telegram-client` call ``.get_input_entity()`` prior to sending the requst to save you from the hassle of doing so manually. That way, convenience calls such as ``client.send_message('lonami', 'hi!')`` From 1fdf976c04ed44044f7215e6f8b3d762c2a85b2b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 27 Mar 2018 18:02:55 +0200 Subject: [PATCH 352/361] Update to v0.18.2 --- readthedocs/extra/changelog.rst | 30 ++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 612547af..e7973363 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,36 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Several bug fixes (v0.18.2) +=========================== + +*Published at 2018/03/27* + +Just a few bug fixes before they become too many. + +Additions +~~~~~~~~~ + +- Getting an entity by its positive ID should be enough, regardless of their + type (whether it's an ``User``, a ``Chat`` or a ``Channel``). Although + wrapping them inside a ``Peer`` is still recommended, it's not necessary. +- New ``client.edit_2fa`` function to change your Two Factor Authentication + settings. +- ``.stringify()`` and string representation for custom ``Dialog/Draft``. + +Bug fixes +~~~~~~~~~ + +- Some bug regarding ``.get_input_peer``. +- ``events.ChatAction`` wasn't picking up all the pins. +- ``force_document=True`` was being ignored for albums. +- Now you're able to send ``Photo`` and ``Document`` as files. +- Wrong access to a member on chat forbidden error for ``.get_participants``. + An empty list is returned instead. +- ``me/self`` check for ``.get[_input]_entity`` has been moved up so if + someone has "me" or "self" as their name they won't be retrieved. + + Iterator methods (v0.18.1) ========================== diff --git a/telethon/version.py b/telethon/version.py index 90266dbf..20c74dd3 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.18.1' +__version__ = '0.18.2' From 755aa363ee87d8615aaaec9bc8f03b4a6a37e5dd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 27 Mar 2018 18:22:01 +0200 Subject: [PATCH 353/361] Update some out of date examples in the documentation --- .../extra/examples/chats-and-channels.rst | 21 ++++++++++----- .../extra/examples/working-with-messages.rst | 26 ++++++++++++------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 7836a348..c64502b2 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -11,10 +11,9 @@ Working with Chats and Channels Joining a chat or channel ************************* -Note that `Chat`__\ s are normal groups, and `Channel`__\ s are a -special form of `Chat`__\ s, -which can also be super-groups if their ``megagroup`` member is -``True``. +Note that :tl:`Chat` are normal groups, and :tl:`Channel` are a +special form of ``Chat``, which can also be super-groups if +their ``megagroup`` member is ``True``. Joining a public channel @@ -101,6 +100,13 @@ __ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html Retrieving all chat members (channels too) ****************************************** +You can use +:obj:`client.get_participants `` +to retrieve the participants (click it to see the relevant parameters). +Most of the time you will just need ``client.get_participants(entity)``. + +This is what said method is doing behind the scenes as an example. + In order to get all the members from a mega-group or channel, you need to use `GetParticipantsRequest`__. As we can see it needs an `InputChannel`__, (passing the mega-group or channel you're going to @@ -134,9 +140,10 @@ a fixed limit: .. note:: - It is **not** possible to get more than 10,000 members from a - group. It's a hard limit impossed by Telegram and there is - nothing you can do about it. Refer to `issue 573`__ for more. + If you need more than 10,000 members from a group you should use the + mentioned ``client.get_participants(..., aggressive=True)``. It will + do some tricks behind the scenes to get as many entities as possible. + Refer to `issue 573`__ for more on this. Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__, diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index e2471a25..5ff04539 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -11,18 +11,27 @@ Working with messages Forwarding messages ******************* -Note that ForwardMessageRequest_ (note it's Message, singular) will *not* -work if channels are involved. This is because channel (and megagroups) IDs -are not unique, so you also need to know who the sender is (a parameter this -request doesn't have). - -Either way, you are encouraged to use ForwardMessagesRequest_ (note it's -Message*s*, plural) *always*, since it is more powerful, as follows: +This request is available as a friendly method through +:obj:`client.forward_messages ``, +and can be used like shown below: .. code-block:: python + # If you only have the message IDs + client.forward_messages( + entity, # to which entity you are forwarding the messages + message_ids, # the IDs of the messages (or message) to forward + from_entity # who sent the messages? + ) + + # If you have ``Message`` objects + client.forward_messages( + entity, # to which entity you are forwarding the messages + messages # the messages (or message) to forward + ) + + # You can also do it manually if you prefer from telethon.tl.functions.messages import ForwardMessagesRequest - # note the s ^ messages = foo() # retrieve a few messages (or even one, in a list) from_entity = bar() @@ -119,7 +128,6 @@ send yourself the very first sticker you have: )) -.. _ForwardMessageRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_message.html .. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html .. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html .. _issues: https://github.com/LonamiWebs/Telethon/issues/215 From de67531f9766e64b9b6076bbaf0d27de62acb71f Mon Sep 17 00:00:00 2001 From: Kyle2142 <19349418+Kyle2142@users.noreply.github.com> Date: Tue, 27 Mar 2018 19:47:10 +0200 Subject: [PATCH 354/361] Add examples for edit_2fa() (#729) --- readthedocs/extra/basic/creating-a-client.rst | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index ba4adc28..7a110e0d 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -150,10 +150,44 @@ The mentioned ``.start()`` method will handle this for you as well, but you must set the ``password=`` parameter beforehand (it won't be asked). If you don't have 2FA enabled, but you would like to do so through the library, -use :obj:`client.edit_2fa ` -for it. Be sure to know what you're doing when using this function and you -won't run into any problems. +use ``client.edit_2fa()``. +Be sure to know what you're doing when using this function and +you won't run into any problems. +Take note that if you want to set only the email/hint and leave +the current password unchanged, you need to "redo" the 2fa. +See the examples below: + + .. code-block:: python + + from telethon.errors import EmailUnconfirmedError + + # Sets 2FA password for first time: + client.edit_2fa(new_password='supersecurepassword') + + # Changes password: + client.edit_2fa(current_password='supersecurepassword', + new_password='changedmymind') + + # Clears current password (i.e. removes 2FA): + client.edit_2fa(current_password='changedmymind', new_password=None) + + # Sets new password with recovery email: + try: + client.edit_2fa(new_password='memes and dreams', + email='JohnSmith@example.com') + # Raises error (you need to check your email to complete 2FA setup.) + except EmailUnconfirmedError: + # You can put email checking code here if desired. + pass + + # Also take note that unless you remove 2FA or explicitly + # give email parameter again it will keep the last used setting + + # Set hint after already setting password: + client.edit_2fa(current_password='memes and dreams', + new_password='memes and dreams', + hint='It keeps you alive') __ https://github.com/Anorov/PySocks#installation __ https://github.com/Anorov/PySocks#usage-1 From dcb7820c5f7046796471531b955290911f2e02a0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 28 Mar 2018 15:52:35 +0200 Subject: [PATCH 355/361] Add a new events.MessageRead --- telethon/events/__init__.py | 132 ++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index ff8f8d48..ce605ea5 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1123,6 +1123,138 @@ class MessageDeleted(_EventBuilder): self.deleted_ids = deleted_ids +@_name_inner_event +class MessageRead(_EventBuilder): + """ + Event fired when one or more messages have been read. + + Args: + inbox (:obj:`bool`, optional): + If this argument is ``True``, then when you read someone else's + messages the event will be fired. By default (``False``) only + when messages you sent are read by someone else will fire it. + """ + def __init__(self, inbox=False, chats=None, blacklist_chats=None): + super().__init__(chats, blacklist_chats) + self.inbox = inbox + + def build(self, update): + if isinstance(update, types.UpdateReadHistoryInbox): + event = MessageRead.Event(update.peer, update.max_id, False) + elif isinstance(update, types.UpdateReadHistoryOutbox): + event = MessageRead.Event(update.peer, update.max_id, True) + elif isinstance(update, types.UpdateReadChannelInbox): + event = MessageRead.Event(types.PeerChannel(update.channel_id), + update.max_id, False) + elif isinstance(update, types.UpdateReadChannelOutbox): + event = MessageRead.Event(types.PeerChannel(update.channel_id), + update.max_id, True) + elif isinstance(update, types.UpdateReadMessagesContents): + event = MessageRead.Event(message_ids=update.messages, + contents=True) + elif isinstance(update, types.UpdateChannelReadMessagesContents): + event = MessageRead.Event(types.PeerChannel(update.channel_id), + message_ids=update.messages, + contents=True) + else: + return + + if self.inbox == event.outbox: + return + + event._entities = update.entities + return self._filter_event(event) + + class Event(_EventCommon): + """ + Represents the event of one or more messages being read. + + Members: + max_id (:obj:`int`): + Up to which message ID has been read. Every message + with an ID equal or lower to it have been read. + + outbox (:obj:`bool`): + ``True`` if someone else has read your messages. + + contents (:obj:`bool`): + ``True`` if what was read were the contents of a message. + This will be the case when e.g. you play a voice note. + It may only be set on ``inbox`` events. + """ + def __init__(self, peer=None, max_id=None, out=False, contents=False, + message_ids=None): + self.outbox = out + self.contents = contents + self._message_ids = message_ids or [] + self._messages = None + self.max_id = max_id or max(message_ids or [], default=None) + super().__init__(peer, self.max_id) + + @property + def inbox(self): + """ + ``True`` if you have read someone else's messages. + """ + return not self.outbox + + @property + def message_ids(self): + """ + The IDs of the messages **which contents'** were read. + + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + return self._message_ids + + @property + def messages(self): + """ + The list of :tl:`Message` **which contents'** were read. + + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + if self._messages is None: + chat = self.input_chat + if not chat: + self._messages = [] + elif isinstance(chat, types.InputPeerChannel): + self._messages =\ + self._client(functions.channels.GetMessagesRequest( + chat, self._message_ids + )).messages + else: + self._messages =\ + self._client(functions.messages.GetMessagesRequest( + self._message_ids + )).messages + + return self._messages + + def is_read(self, message): + """ + Returns ``True`` if the given message (or its ID) has been read. + + If a list-like argument is provided, this method will return a + list of booleans indicating which messages have been read. + """ + if utils.is_list_like(message): + return [(m if isinstance(m, int) else m.id) <= self.max_id + for m in message] + else: + return (message if isinstance(message, int) + else message.id) <= self.max_id + + def __contains__(self, message): + """``True`` if the message(s) are read message.""" + if utils.is_list_like(message): + return all(self.is_read(message)) + else: + return self.is_read(message) + + class StopPropagation(Exception): """ If this exception is raised in any of the handlers for a given event, From 395e7025865479244e6f9c9cba64cbad596a5642 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 28 Mar 2018 16:03:47 +0200 Subject: [PATCH 356/361] Make py:obj default Sphinx's role --- readthedocs/conf.py | 3 + .../extra/examples/chats-and-channels.rst | 2 +- .../extra/examples/working-with-messages.rst | 2 +- telethon/events/__init__.py | 80 +++---- telethon/telegram_client.py | 217 +++++++++--------- telethon/tl/custom/dialog.py | 16 +- telethon/tl/custom/draft.py | 6 +- 7 files changed, 164 insertions(+), 162 deletions(-) diff --git a/readthedocs/conf.py b/readthedocs/conf.py index 27f1ae52..2821e069 100644 --- a/readthedocs/conf.py +++ b/readthedocs/conf.py @@ -42,6 +42,9 @@ extensions = [ 'custom_roles' ] +# Change the default role so we can avoid prefixing everything with :obj: +default_role = "py:obj" + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index c64502b2..9851282f 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -101,7 +101,7 @@ Retrieving all chat members (channels too) ****************************************** You can use -:obj:`client.get_participants `` +`client.get_participants ` to retrieve the participants (click it to see the relevant parameters). Most of the time you will just need ``client.get_participants(entity)``. diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index 5ff04539..3db1aed0 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -12,7 +12,7 @@ Forwarding messages ******************* This request is available as a friendly method through -:obj:`client.forward_messages ``, +`client.forward_messages `, and can be used like shown below: .. code-block:: python diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index ce605ea5..1364e89e 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -46,11 +46,11 @@ class _EventBuilder(abc.ABC): The common event builder, with builtin support to filter per chat. Args: - chats (:obj:`entity`, optional): + chats (`entity`, optional): May be one or more entities (username/peer/etc.). By default, only matching chats will be handled. - blacklist_chats (:obj:`bool`, optional): + blacklist_chats (`bool`, optional): Whether to treat the chats as a blacklist instead of as a whitelist (default). This means that every chat will be handled *except* those specified in ``chats`` @@ -229,15 +229,15 @@ class NewMessage(_EventBuilder): Represents a new message event builder. Args: - incoming (:obj:`bool`, optional): + incoming (`bool`, optional): If set to ``True``, only **incoming** messages will be handled. Mutually exclusive with ``outgoing`` (can only set one of either). - outgoing (:obj:`bool`, optional): + outgoing (`bool`, optional): If set to ``True``, only **outgoing** messages will be handled. Mutually exclusive with ``incoming`` (can only set one of either). - pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional): + pattern (`str`, `callable`, `Pattern`, optional): If set, only messages matching this pattern will be handled. You can specify a regex-like string which will be matched against the message, a callable function that returns ``True`` @@ -331,16 +331,16 @@ class NewMessage(_EventBuilder): message (:tl:`Message`): This is the original :tl:`Message` object. - is_private (:obj:`bool`): + is_private (`bool`): True if the message was sent as a private message. - is_group (:obj:`bool`): + is_group (`bool`): True if the message was sent on a group or megagroup. - is_channel (:obj:`bool`): + is_channel (`bool`): True if the message was sent on a megagroup or channel. - is_reply (:obj:`str`): + is_reply (`str`): Whether the message is a reply to some other or not. """ def __init__(self, message): @@ -699,35 +699,35 @@ class ChatAction(_EventBuilder): Represents the event of a new chat action. Members: - new_pin (:obj:`bool`): + new_pin (`bool`): ``True`` if there is a new pin. - new_photo (:obj:`bool`): + new_photo (`bool`): ``True`` if there's a new chat photo (or it was removed). photo (:tl:`Photo`, optional): The new photo (or ``None`` if it was removed). - user_added (:obj:`bool`): + user_added (`bool`): ``True`` if the user was added by some other. - user_joined (:obj:`bool`): + user_joined (`bool`): ``True`` if the user joined on their own. - user_left (:obj:`bool`): + user_left (`bool`): ``True`` if the user left on their own. - user_kicked (:obj:`bool`): + user_kicked (`bool`): ``True`` if the user was kicked by some other. - created (:obj:`bool`, optional): + created (`bool`, optional): ``True`` if this chat was just created. - new_title (:obj:`bool`, optional): + new_title (`bool`, optional): The new title string for the chat, if applicable. - unpin (:obj:`bool`): + unpin (`bool`): ``True`` if the existing pin gets unpinned. """ def __init__(self, where, new_pin=None, new_photo=None, @@ -944,62 +944,62 @@ class UserUpdate(_EventBuilder): Represents the event of an user status update (last seen, joined). Members: - online (:obj:`bool`, optional): + online (`bool`, optional): ``True`` if the user is currently online, ``False`` otherwise. Might be ``None`` if this information is not present. - last_seen (:obj:`datetime`, optional): + last_seen (`datetime`, optional): Exact date when the user was last seen if known. - until (:obj:`datetime`, optional): + until (`datetime`, optional): Until when will the user remain online. - within_months (:obj:`bool`): + within_months (`bool`): ``True`` if the user was seen within 30 days. - within_weeks (:obj:`bool`): + within_weeks (`bool`): ``True`` if the user was seen within 7 days. - recently (:obj:`bool`): + recently (`bool`): ``True`` if the user was seen within a day. action (:tl:`SendMessageAction`, optional): The "typing" action if any the user is performing if any. - cancel (:obj:`bool`): + cancel (`bool`): ``True`` if the action was cancelling other actions. - typing (:obj:`bool`): + typing (`bool`): ``True`` if the action is typing a message. - recording (:obj:`bool`): + recording (`bool`): ``True`` if the action is recording something. - uploading (:obj:`bool`): + uploading (`bool`): ``True`` if the action is uploading something. - playing (:obj:`bool`): + playing (`bool`): ``True`` if the action is playing a game. - audio (:obj:`bool`): + audio (`bool`): ``True`` if what's being recorded/uploaded is an audio. - round (:obj:`bool`): + round (`bool`): ``True`` if what's being recorded/uploaded is a round video. - video (:obj:`bool`): + video (`bool`): ``True`` if what's being recorded/uploaded is an video. - document (:obj:`bool`): + document (`bool`): ``True`` if what's being uploaded is document. - geo (:obj:`bool`): + geo (`bool`): ``True`` if what's being uploaded is a geo. - photo (:obj:`bool`): + photo (`bool`): ``True`` if what's being uploaded is a photo. - contact (:obj:`bool`): + contact (`bool`): ``True`` if what's being uploaded (selected) is a contact. """ def __init__(self, user_id, status=None, typing=None): @@ -1129,7 +1129,7 @@ class MessageRead(_EventBuilder): Event fired when one or more messages have been read. Args: - inbox (:obj:`bool`, optional): + inbox (`bool`, optional): If this argument is ``True``, then when you read someone else's messages the event will be fired. By default (``False``) only when messages you sent are read by someone else will fire it. @@ -1170,14 +1170,14 @@ class MessageRead(_EventBuilder): Represents the event of one or more messages being read. Members: - max_id (:obj:`int`): + max_id (`int`): Up to which message ID has been read. Every message with an ID equal or lower to it have been read. - outbox (:obj:`bool`): + outbox (`bool`): ``True`` if someone else has read your messages. - contents (:obj:`bool`): + contents (`bool`): ``True`` if what was read were the contents of a message. This will be the case when e.g. you play a voice note. It may only be set on ``inbox`` events. diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9c649a0a..ca072f4f 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -97,52 +97,51 @@ class TelegramClient(TelegramBareClient): Initializes the Telegram client with the specified API ID and Hash. Args: - session (:obj:`str` | :obj:`telethon.sessions.abstract.Session`, \ - :obj:`None`): + session (`str` | `telethon.sessions.abstract.Session`, `None`): The file name of the session file to be used if a string is given (it may be a full path), or the Session instance to be used otherwise. If it's ``None``, the session will not be saved, and you should call :meth:`.log_out()` when you're done. - api_id (:obj:`int` | :obj:`str`): + api_id (`int` | `str`): The API ID you obtained from https://my.telegram.org. - api_hash (:obj:`str`): + api_hash (`str`): The API ID you obtained from https://my.telegram.org. - connection_mode (:obj:`ConnectionMode`, optional): + connection_mode (`ConnectionMode`, optional): The connection mode to be used when creating a new connection to the servers. Defaults to the ``TCP_FULL`` mode. This will only affect how messages are sent over the network and how much processing is required before sending them. - use_ipv6 (:obj:`bool`, optional): + use_ipv6 (`bool`, optional): Whether to connect to the servers through IPv6 or not. By default this is ``False`` as IPv6 support is not too widespread yet. - proxy (:obj:`tuple` | :obj:`dict`, optional): + proxy (`tuple` | `dict`, optional): A tuple consisting of ``(socks.SOCKS5, 'host', port)``. See https://github.com/Anorov/PySocks#usage-1 for more. - update_workers (:obj:`int`, optional): + update_workers (`int`, optional): If specified, represents how many extra threads should be spawned to handle incoming updates, and updates will be kept in memory until they are processed. Note that you must set this to at least ``0`` if you want to be able to process updates through :meth:`updates.poll()`. - timeout (:obj:`int` | :obj:`float` | :obj:`timedelta`, optional): + timeout (`int` | `float` | `timedelta`, optional): The timeout to be used when receiving responses from the network. Defaults to 5 seconds. - spawn_read_thread (:obj:`bool`, optional): + spawn_read_thread (`bool`, optional): Whether to use an extra background thread or not. Defaults to ``True`` so receiving items from the network happens instantly, as soon as they arrive. Can still be disabled if you want to run the library without any additional thread. - report_errors (:obj:`bool`, optional): + report_errors (`bool`, optional): Whether to report RPC errors or not. Defaults to ``True``, see :ref:`api-status` for more information. @@ -207,10 +206,10 @@ class TelegramClient(TelegramBareClient): Sends a code request to the specified phone number. Args: - phone (:obj:`str` | :obj:`int`): + phone (`str` | `int`): The phone to which the code will be sent. - force_sms (:obj:`bool`, optional): + force_sms (`bool`, optional): Whether to force sending as SMS. Returns: @@ -249,36 +248,36 @@ class TelegramClient(TelegramBareClient): (You are now logged in) Args: - phone (:obj:`str` | :obj:`int` | :obj:`callable`): + phone (`str` | `int` | `callable`): The phone (or callable without arguments to get it) to which the code will be sent. - password (:obj:`callable`, optional): + password (`callable`, optional): The password for 2 Factor Authentication (2FA). This is only required if it is enabled in your account. - bot_token (:obj:`str`): + bot_token (`str`): Bot Token obtained by `@BotFather `_ to log in as a bot. Cannot be specified with ``phone`` (only one of either allowed). - force_sms (:obj:`bool`, optional): + force_sms (`bool`, optional): Whether to force sending the code request as SMS. This only makes sense when signing in with a `phone`. - code_callback (:obj:`callable`, optional): + code_callback (`callable`, optional): A callable that will be used to retrieve the Telegram login code. Defaults to `input()`. - first_name (:obj:`str`, optional): + first_name (`str`, optional): The first name to be used if signing up. This has no effect if the account already exists and you sign in. - last_name (:obj:`str`, optional): + last_name (`str`, optional): Similar to the first name, but for the last. Optional. Returns: - This :obj:`TelegramClient`, so initialization + This `TelegramClient`, so initialization can be chained with ``.start()``. """ @@ -369,26 +368,26 @@ class TelegramClient(TelegramBareClient): or code that Telegram sent. Args: - phone (:obj:`str` | :obj:`int`): + phone (`str` | `int`): The phone to send the code to if no code was provided, or to override the phone that was previously used with these requests. - code (:obj:`str` | :obj:`int`): + code (`str` | `int`): The code that Telegram sent. Note that if you have sent this code through the application itself it will immediately expire. If you want to send the code, obfuscate it somehow. If you're not doing any of this you can ignore this note. - password (:obj:`str`): + password (`str`): 2FA password, should be used if a previous call raised SessionPasswordNeededError. - bot_token (:obj:`str`): + bot_token (`str`): Used to sign in as a bot. Not all requests will be available. This should be the hash the @BotFather gave you. - phone_code_hash (:obj:`str`): + phone_code_hash (`str`): The hash returned by .send_code_request. This can be set to None to use the last hash known. @@ -445,13 +444,13 @@ class TelegramClient(TelegramBareClient): You must call .send_code_request(phone) first. Args: - code (:obj:`str` | :obj:`int`): + code (`str` | `int`): The code sent by Telegram - first_name (:obj:`str`): + first_name (`str`): The first name to be used by the new account. - last_name (:obj:`str`, optional) + last_name (`str`, optional) Optional last name. Returns: @@ -497,7 +496,7 @@ class TelegramClient(TelegramBareClient): or None if the request fails (hence, not authenticated). Args: - input_peer (:obj:`bool`, optional): + input_peer (`bool`, optional): Whether to return the :tl:`InputPeerUser` version or the normal :tl:`User`. This can be useful if you just need to know the ID of yourself. @@ -530,27 +529,27 @@ class TelegramClient(TelegramBareClient): Dialogs are the open "chats" or conversations with other people. Args: - limit (:obj:`int` | :obj:`None`): + limit (`int` | `None`): How many dialogs to be retrieved as maximum. Can be set to ``None`` to retrieve all dialogs. Note that this may take whole minutes if you have hundreds of dialogs, as Telegram will tell the library to slow down through a ``FloodWaitError``. - offset_date (:obj:`datetime`, optional): + offset_date (`datetime`, optional): The offset date to be used. - offset_id (:obj:`int`, optional): + offset_id (`int`, optional): The message ID to be used as an offset. offset_peer (:tl:`InputPeer`, optional): The peer to be used as an offset. - _total (:obj:`list`, optional): + _total (`list`, optional): A single-item list to pass the total parameter by reference. Yields: - Instances of :obj:`telethon.tl.custom.dialog.Dialog`. + Instances of `telethon.tl.custom.dialog.Dialog`. """ limit = float('inf') if limit is None else int(limit) if limit == 0: @@ -618,9 +617,9 @@ class TelegramClient(TelegramBareClient): """ Iterator over all open draft messages. - Instances of :obj:`telethon.tl.custom.draft.Draft` are yielded. - You can call :obj:`telethon.tl.custom.draft.Draft.set_message` - to change the message or :obj:`telethon.tl.custom.draft.Draft.delete` + Instances of `telethon.tl.custom.draft.Draft` are yielded. + You can call `telethon.tl.custom.draft.Draft.set_message` + to change the message or `telethon.tl.custom.draft.Draft.delete` among other things. """ for update in self(GetAllDraftsRequest()).updates: @@ -711,33 +710,33 @@ class TelegramClient(TelegramBareClient): Sends the given message to the specified entity (user/chat/channel). Args: - entity (:obj:`entity`): + entity (`entity`): To who will it be sent. - message (:obj:`str` | :tl:`Message`): + message (`str` | :tl:`Message`): The message to be sent, or another message object to resend. - reply_to (:obj:`int` | :tl:`Message`, optional): + reply_to (`int` | :tl:`Message`, optional): Whether to reply to a message or not. If an integer is provided, it should be the ID of the message that it should reply to. - parse_mode (:obj:`str`, optional): + parse_mode (`str`, optional): Can be 'md' or 'markdown' for markdown-like parsing (default), or 'htm' or 'html' for HTML-like parsing. If ``None`` or any other false-y value is provided, the message will be sent with no formatting. - link_preview (:obj:`bool`, optional): + link_preview (`bool`, optional): Should the link preview be shown? - file (:obj:`file`, optional): + file (`file`, optional): Sends a message with a file attached (e.g. a photo, video, audio or document). The ``message`` may be empty. - force_document (:obj:`bool`, optional): + force_document (`bool`, optional): Whether to send the given file as a document or not. - clear_draft (:obj:`bool`, optional): + clear_draft (`bool`, optional): Whether the existing draft should be cleared or not. Has no effect when sending a file. @@ -805,13 +804,13 @@ class TelegramClient(TelegramBareClient): Forwards the given message(s) to the specified entity. Args: - entity (:obj:`entity`): + entity (`entity`): To which entity the message(s) will be forwarded. - messages (:obj:`list` | :obj:`int` | :tl:`Message`): + messages (`list` | `int` | :tl:`Message`): The message(s) to forward, or their integer IDs. - from_peer (:obj:`entity`): + from_peer (`entity`): If the given messages are integer IDs and not instances of the ``Message`` class, this *must* be specified in order for the forward to work. @@ -858,22 +857,22 @@ class TelegramClient(TelegramBareClient): Edits the given message ID (to change its contents or disable preview). Args: - entity (:obj:`entity`): + entity (`entity`): From which chat to edit the message. - message_id (:obj:`str`): + message_id (`str`): The ID of the message (or ``Message`` itself) to be edited. - message (:obj:`str`, optional): + message (`str`, optional): The new text of the message. - parse_mode (:obj:`str`, optional): + parse_mode (`str`, optional): Can be 'md' or 'markdown' for markdown-like parsing (default), or 'htm' or 'html' for HTML-like parsing. If ``None`` or any other false-y value is provided, the message will be sent with no formatting. - link_preview (:obj:`bool`, optional): + link_preview (`bool`, optional): Should the link preview be shown? Raises: @@ -902,15 +901,15 @@ class TelegramClient(TelegramBareClient): Deletes a message from a chat, optionally "for everyone". Args: - entity (:obj:`entity`): + entity (`entity`): From who the message will be deleted. This can actually be ``None`` for normal chats, but **must** be present for channels and megagroups. - message_ids (:obj:`list` | :obj:`int` | :tl:`Message`): + message_ids (`list` | `int` | :tl:`Message`): The IDs (or ID) or messages to be deleted. - revoke (:obj:`bool`, optional): + revoke (`bool`, optional): Whether the message should be deleted for everyone or not. By default it has the opposite behaviour of official clients, and it will delete the message for everyone. @@ -944,48 +943,48 @@ class TelegramClient(TelegramBareClient): Iterator over the message history for the specified entity. Args: - entity (:obj:`entity`): + entity (`entity`): The entity from whom to retrieve the message history. - limit (:obj:`int` | :obj:`None`, optional): + limit (`int` | `None`, optional): Number of messages to be retrieved. Due to limitations with the API retrieving more than 3000 messages will take longer than half a minute (or even more based on previous calls). The limit may also be ``None``, which would eventually return the whole history. - offset_date (:obj:`datetime`): + offset_date (`datetime`): Offset date (messages *previous* to this date will be retrieved). Exclusive. - offset_id (:obj:`int`): + offset_id (`int`): Offset message ID (only messages *previous* to the given ID will be retrieved). Exclusive. - max_id (:obj:`int`): + max_id (`int`): All the messages with a higher (newer) ID or equal to this will be excluded - min_id (:obj:`int`): + min_id (`int`): All the messages with a lower (older) ID or equal to this will be excluded. - add_offset (:obj:`int`): + add_offset (`int`): Additional message offset (all of the specified offsets + this offset = older messages). - batch_size (:obj:`int`): + batch_size (`int`): Messages will be returned in chunks of this size (100 is the maximum). While it makes no sense to modify this value, you are still free to do so. - wait_time (:obj:`int`): + wait_time (`int`): Wait time between different :tl:`GetHistoryRequest`. Use this parameter to avoid hitting the ``FloodWaitError`` as needed. If left to ``None``, it will default to 1 second only if the limit is higher than 3000. - _total (:obj:`list`, optional): + _total (`list`, optional): A single-item list to pass the total parameter by reference. Yields: @@ -1100,17 +1099,17 @@ class TelegramClient(TelegramBareClient): read their messages, also known as the "double check"). Args: - entity (:obj:`entity`): + entity (`entity`): The chat where these messages are located. - message (:obj:`list` | :tl:`Message`): + message (`list` | :tl:`Message`): Either a list of messages or a single message. - max_id (:obj:`int`): + max_id (`int`): Overrides messages, until which message should the acknowledge should be sent. - clear_mentions (:obj:`bool`): + clear_mentions (`bool`): Whether the mention badge should be cleared (so that there are no more mentions) or not for the given entity. @@ -1165,13 +1164,13 @@ class TelegramClient(TelegramBareClient): Iterator over the participants belonging to the specified chat. Args: - entity (:obj:`entity`): + entity (`entity`): The entity from which to retrieve the participants list. - limit (:obj:`int`): + limit (`int`): Limits amount of participants fetched. - search (:obj:`str`, optional): + search (`str`, optional): Look for participants with this string in name/username. filter (:tl:`ChannelParticipantsFilter`, optional): @@ -1179,7 +1178,7 @@ class TelegramClient(TelegramBareClient): Note that you might not have permissions for some filter. This has no effect for normal chats or users. - aggressive (:obj:`bool`, optional): + aggressive (`bool`, optional): Aggressively looks for all participants in the chat in order to get more than 10,000 members (a hard limit imposed by Telegram). Note that this might take a long @@ -1189,7 +1188,7 @@ class TelegramClient(TelegramBareClient): This has no effect for groups or channels with less than 10,000 members, or if a ``filter`` is given. - _total (:obj:`list`, optional): + _total (`list`, optional): A single-item list to pass the total parameter by reference. Yields: @@ -1336,10 +1335,10 @@ class TelegramClient(TelegramBareClient): Sends a file to the specified entity. Args: - entity (:obj:`entity`): + entity (`entity`): Who will receive the file. - file (:obj:`str` | :obj:`bytes` | :obj:`file` | :obj:`media`): + file (`str` | `bytes` | `file` | `media`): The path of the file, byte array, or stream that will be sent. 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 @@ -1356,35 +1355,35 @@ class TelegramClient(TelegramBareClient): sent as an album in the order in which they appear, sliced in chunks of 10 if more than 10 are given. - caption (:obj:`str`, optional): + caption (`str`, optional): Optional caption for the sent media message. - force_document (:obj:`bool`, optional): + force_document (`bool`, optional): If left to ``False`` and the file is a path that ends with the extension of an image file or a video file, it will be sent as such. Otherwise always as a document. - progress_callback (:obj:`callable`, optional): + progress_callback (`callable`, optional): A callback function accepting two parameters: ``(sent bytes, total)``. - reply_to (:obj:`int` | :tl:`Message`): + reply_to (`int` | :tl:`Message`): Same as reply_to from .send_message(). - attributes (:obj:`list`, optional): + attributes (`list`, optional): Optional attributes that override the inferred ones, like :tl:`DocumentAttributeFilename` and so on. - thumb (:obj:`str` | :obj:`bytes` | :obj:`file`, optional): + thumb (`str` | `bytes` | `file`, optional): Optional thumbnail (for videos). - allow_cache (:obj:`bool`, optional): + allow_cache (`bool`, optional): Whether to allow using the cached version stored in the database or not. Defaults to ``True`` to avoid re-uploads. Must be ``False`` if you wish to use different attributes or thumb than those that were used when the file was cached. - parse_mode (:obj:`str`, optional): + parse_mode (`str`, optional): The parse mode for the caption message. Kwargs: @@ -1628,7 +1627,7 @@ class TelegramClient(TelegramBareClient): will **not** upload the file to your own chat or any chat at all. Args: - file (:obj:`str` | :obj:`bytes` | :obj:`file`): + file (`str` | `bytes` | `file`): The path of the file, byte array, or stream that will be sent. 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 @@ -1637,23 +1636,23 @@ class TelegramClient(TelegramBareClient): Subsequent calls with the very same file will result in immediate uploads, unless ``.clear_file_cache()`` is called. - part_size_kb (:obj:`int`, optional): + part_size_kb (`int`, optional): Chunk size when uploading files. The larger, the less requests will be made (up to 512KB maximum). - file_name (:obj:`str`, optional): + file_name (`str`, optional): The file name which will be used on the resulting InputFile. If not specified, the name will be taken from the ``file`` and if this is not a ``str``, it will be ``"unnamed"``. - use_cache (:obj:`type`, optional): + use_cache (`type`, optional): The type of cache to use (currently either ``InputDocument`` or ``InputPhoto``). If present and the file is small enough to need the MD5, it will be checked against the database, and if a match is found, the upload won't be made. Instead, an instance of type ``use_cache`` will be returned. - progress_callback (:obj:`callable`, optional): + progress_callback (`callable`, optional): A callback function accepting two parameters: ``(sent bytes, total)``. @@ -1756,14 +1755,14 @@ class TelegramClient(TelegramBareClient): Downloads the profile photo of the given entity (user/chat/channel). Args: - entity (:obj:`entity`): + entity (`entity`): From who the photo will be downloaded. - file (:obj:`str` | :obj:`file`, optional): + file (`str` | `file`, optional): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. - download_big (:obj:`bool`, optional): + download_big (`bool`, optional): Whether to use the big version of the available photos. Returns: @@ -1845,11 +1844,11 @@ class TelegramClient(TelegramBareClient): message (:tl:`Message` | :tl:`Media`): The media or message containing the media that will be downloaded. - file (:obj:`str` | :obj:`file`, optional): + file (`str` | `file`, optional): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. - progress_callback (:obj:`callable`, optional): + progress_callback (`callable`, optional): A callback function accepting two parameters: ``(received bytes, total)``. @@ -2074,19 +2073,19 @@ class TelegramClient(TelegramBareClient): input_location (:tl:`InputFileLocation`): The file location from which the file will be downloaded. - file (:obj:`str` | :obj:`file`): + file (`str` | `file`): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. - part_size_kb (:obj:`int`, optional): + part_size_kb (`int`, optional): Chunk size when downloading files. The larger, the less requests will be made (up to 512KB maximum). - file_size (:obj:`int`, optional): + file_size (`int`, optional): The file size that is about to be downloaded, if known. Only used if ``progress_callback`` is specified. - progress_callback (:obj:`callable`, optional): + progress_callback (`callable`, optional): A callback function accepting two parameters: ``(downloaded bytes, total)``. Note that the ``total`` is the provided ``file_size``. @@ -2179,7 +2178,7 @@ class TelegramClient(TelegramBareClient): Decorator helper method around add_event_handler(). Args: - event (:obj:`_EventBuilder` | :obj:`type`): + event (`_EventBuilder` | `type`): The event builder class or instance to be used, for instance ``events.NewMessage``. """ @@ -2215,10 +2214,10 @@ class TelegramClient(TelegramBareClient): Registers the given callback to be called on the specified event. Args: - callback (:obj:`callable`): + callback (`callable`): The callable function accepting one parameter to be used. - event (:obj:`_EventBuilder` | :obj:`type`, optional): + event (`_EventBuilder` | `type`, optional): The event builder class or instance to be used, for instance ``events.NewMessage``. @@ -2299,7 +2298,7 @@ class TelegramClient(TelegramBareClient): """ Turns the given entity into a valid Telegram user or chat. - entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`): + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): The entity (or iterable of entities) to be transformed. If it's a string which can be converted to an integer or starts with '+' it will be resolved as if it were a phone number. @@ -2416,7 +2415,7 @@ class TelegramClient(TelegramBareClient): use this kind of InputUser, InputChat and so on, so this is the most suitable call to make for those cases. - entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`): + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): The integer ID of an user or otherwise either of a :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for which to get its ``Input*`` version. @@ -2485,23 +2484,23 @@ class TelegramClient(TelegramBareClient): Has no effect if both current and new password are omitted. - current_password (:obj:`str`, optional): + current_password (`str`, optional): The current password, to authorize changing to ``new_password``. Must be set if changing existing 2FA settings. Must **not** be set if 2FA is currently disabled. Passing this by itself will remove 2FA (if correct). - new_password (:obj:`str`, optional): + new_password (`str`, optional): The password to set as 2FA. If 2FA was already enabled, ``current_password`` **must** be set. Leaving this blank or ``None`` will remove the password. - hint (:obj:`str`, optional): + hint (`str`, optional): Hint to be displayed by Telegram when it asks for 2FA. Leaving unspecified is highly discouraged. Has no effect if ``new_password`` is not set. - email (:obj:`str`, optional): + email (`str`, optional): Recovery and verification email. Raises ``EmailUnconfirmedError`` if value differs from current one, and has no effect if ``new_password`` is not set. diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index 921a4c34..74b2598a 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -14,7 +14,7 @@ class Dialog: dialog (:tl:`Dialog`): The original ``Dialog`` instance. - pinned (:obj:`bool`): + pinned (`bool`): Whether this dialog is pinned to the top or not. message (:tl:`Message`): @@ -22,31 +22,31 @@ class Dialog: will not be updated when new messages arrive, it's only set on creation of the instance. - date (:obj:`datetime`): + date (`datetime`): The date of the last message sent on this dialog. - entity (:obj:`entity`): + entity (`entity`): The entity that belongs to this dialog (user, chat or channel). input_entity (:tl:`InputPeer`): Input version of the entity. - id (:obj:`int`): + id (`int`): The marked ID of the entity, which is guaranteed to be unique. - name (:obj:`str`): + name (`str`): Display name for this dialog. For chats and channels this is their title, and for users it's "First-Name Last-Name". - unread_count (:obj:`int`): + unread_count (`int`): How many messages are currently unread in this dialog. Note that this value won't update when new messages arrive. - unread_mentions_count (:obj:`int`): + unread_mentions_count (`int`): How many mentions are currently unread in this dialog. Note that this value won't update when new messages arrive. - draft (:obj:`telethon.tl.custom.draft.Draft`): + draft (`telethon.tl.custom.draft.Draft`): The draft object in this dialog. It will not be ``None``, so you can call ``draft.set_message(...)``. """ diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index 09c0e6ac..c83cea37 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -14,13 +14,13 @@ class Draft: instances of this class when calling :meth:`get_drafts()`. Args: - date (:obj:`datetime`): + date (`datetime`): The date of the draft. - link_preview (:obj:`bool`): + link_preview (`bool`): Whether the link preview is enabled or not. - reply_to_msg_id (:obj:`int`): + reply_to_msg_id (`int`): The message ID that the draft will reply to. """ def __init__(self, client, peer, draft): From dede5520dd117a221987343150078c139bf5d69d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 29 Mar 2018 00:56:05 +0200 Subject: [PATCH 357/361] Rename .entities -> ._entities from 7e9d19d to avoid collision --- telethon/events/__init__.py | 12 ++++++------ telethon/update_state.py | 9 +++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 1364e89e..8ae63ea9 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -301,7 +301,7 @@ class NewMessage(_EventBuilder): else: return - event._entities = update.entities + event._entities = update._entities return self._message_filter_event(event) def _message_filter_event(self, event): @@ -691,7 +691,7 @@ class ChatAction(_EventBuilder): else: return - event._entities = update.entities + event._entities = update._entities return self._filter_event(event) class Event(_EventCommon): @@ -936,7 +936,7 @@ class UserUpdate(_EventBuilder): else: return - event._entities = update.entities + event._entities = update._entities return self._filter_event(event) class Event(_EventCommon): @@ -1085,7 +1085,7 @@ class MessageEdited(NewMessage): else: return - event._entities = update.entities + event._entities = update._entities return self._message_filter_event(event) class Event(NewMessage.Event): @@ -1111,7 +1111,7 @@ class MessageDeleted(_EventBuilder): else: return - event._entities = update.entities + event._entities = update._entities return self._filter_event(event) class Event(_EventCommon): @@ -1162,7 +1162,7 @@ class MessageRead(_EventBuilder): if self.inbox == event.outbox: return - event._entities = update.entities + event._entities = update._entities return self._filter_event(event) class Event(_EventCommon): diff --git a/telethon/update_state.py b/telethon/update_state.py index 509697a0..e679e99e 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -129,12 +129,9 @@ class UpdateState: # After running the script for over an hour and receiving over # 1000 updates, the only duplicates received were users going # online or offline. We can trust the server until new reports. - # - # TODO Note somewhere that all updates are modified to include - # .entities, which is a dictionary you can access but may be empty. # This should only be used as read-only. if isinstance(update, tl.UpdateShort): - update.update.entities = {} + update.update._entities = {} self._updates.put(update.update) # Expand "Updates" into "Update", and pass these to callbacks. # Since .users and .chats have already been processed, we @@ -143,9 +140,9 @@ class UpdateState: entities = {utils.get_peer_id(x): x for x in itertools.chain(update.users, update.chats)} for u in update.updates: - u.entities = entities + u._entities = entities self._updates.put(u) # TODO Handle "tl.UpdatesTooLong" else: - update.entities = {} + update._entities = {} self._updates.put(update) From 2b9babb30f8a05dbfd925eddc61cf515a26f55ff Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 30 Mar 2018 11:50:41 +0200 Subject: [PATCH 358/361] Handle GzipPacked lost requests & possibly fix reading normal Reading normal "lost" requests didn't .seek(-4) to read the TLObject again. Now it has been slightly refactored to seek back always and only seek forward when needed (e.g. rpc error). --- telethon/network/mtproto_sender.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 8206fcaa..9b0be18a 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -476,11 +476,13 @@ class MtProtoSender: reader.read_int(signed=False) # code request_id = reader.read_long() inner_code = reader.read_int(signed=False) + reader.seek(-4) __log__.debug('Received response for request with ID %d', request_id) request = self._pop_request(request_id) if inner_code == 0x2144ca19: # RPC Error + reader.seek(4) if self.session.report_errors and request: error = rpc_message_to_error( reader.read_int(), reader.tgread_string(), @@ -505,12 +507,10 @@ class MtProtoSender: return True # All contents were read okay elif request: - if inner_code == 0x3072cfa1: # GZip packed - unpacked_data = gzip.decompress(reader.tgread_bytes()) - with BinaryReader(unpacked_data) as compressed_reader: + if inner_code == GzipPacked.CONSTRUCTOR_ID: + with BinaryReader(GzipPacked.read(reader)) as compressed_reader: request.on_response(compressed_reader) else: - reader.seek(-4) request.on_response(reader) self.session.process_entities(request.result) @@ -525,10 +525,17 @@ class MtProtoSender: # session, it will be skipped by the handle_container(). # For some reason this also seems to happen when downloading # photos, where the server responds with FileJpeg(). - try: - obj = reader.tgread_object() - except Exception as e: - obj = '(failed to read: %s)' % e + def _try_read(r): + try: + return r.tgread_object() + except Exception as e: + return '(failed to read: {})'.format(e) + + if inner_code == GzipPacked.CONSTRUCTOR_ID: + with BinaryReader(GzipPacked.read(reader)) as compressed_reader: + obj = _try_read(compressed_reader) + else: + obj = _try_read(reader) __log__.warning( 'Lost request (ID %d) with code %s will be skipped, contents: %s', From 500792975e3a3e42192d544da7ac67c452d78b17 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 30 Mar 2018 12:18:18 +0200 Subject: [PATCH 359/361] Handle AUTH_KEY_DUPLICATED on connection --- telethon/errors/__init__.py | 3 +++ telethon/errors/rpc_base_errors.py | 13 +++++++++++++ telethon/telegram_bare_client.py | 11 ++++++++++- telethon_generator/error_generator.py | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index 9126aca3..d9875849 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -78,6 +78,9 @@ def rpc_message_to_error(code, message, report_method=None): if code == 404: return NotFoundError(message) + if code == 406: + return AuthKeyError(message) + if code == 500: return ServerError(message) diff --git a/telethon/errors/rpc_base_errors.py b/telethon/errors/rpc_base_errors.py index 467b256c..d2db5439 100644 --- a/telethon/errors/rpc_base_errors.py +++ b/telethon/errors/rpc_base_errors.py @@ -56,6 +56,19 @@ class NotFoundError(RPCError): self.message = message +class AuthKeyError(RPCError): + """ + Errors related to invalid authorization key, like + AUTH_KEY_DUPLICATED which can cause the connection to fail. + """ + code = 406 + message = 'AUTH_KEY' + + def __init__(self, message): + super().__init__(message) + self.message = message + + class FloodError(RPCError): """ The maximum allowed number of attempts to invoke the given method diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 8281f869..a8a43774 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -11,7 +11,7 @@ from .crypto import rsa from .errors import ( RPCError, BrokenAuthKeyError, ServerError, FloodWaitError, FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError, - PhoneMigrateError, NetworkMigrateError, UserMigrateError + PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode from .sessions import Session, SQLiteSession @@ -227,6 +227,15 @@ class TelegramBareClient: self.disconnect() return self.connect(_sync_updates=_sync_updates) + except AuthKeyError as e: + # As of late March 2018 there were two AUTH_KEY_DUPLICATED + # reports. Retrying with a clean auth_key should fix this. + __log__.warning('Auth key error %s. Clearing it and retrying.', e) + self.disconnect() + self.session.auth_key = None + self.session.save() + return self.connect(_sync_updates=_sync_updates) + except (RPCError, ConnectionError) as e: # Probably errors from the previous session, ignore them __log__.error('Connection failed due to %s', e) diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py index 73fb5c5a..4aad78ec 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -11,6 +11,7 @@ known_base_classes = { 401: 'UnauthorizedError', 403: 'ForbiddenError', 404: 'NotFoundError', + 406: 'AuthKeyError', 420: 'FloodError', 500: 'ServerError', } From 7bc021bba45528137bd25b3bb67678f8fddd0b5e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 30 Mar 2018 20:28:07 +0200 Subject: [PATCH 360/361] Update to layer 75 (again) --- telethon/events/__init__.py | 24 +++++++++++++++++------- telethon_generator/scheme.tl | 19 +++++++++++++++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 8ae63ea9..d440ef7c 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -118,11 +118,15 @@ class _EventCommon(abc.ABC): try: if isinstance(chat, types.InputPeerChannel): result = self._client( - functions.channels.GetMessagesRequest(chat, [msg_id]) + functions.channels.GetMessagesRequest(chat, [ + types.InputMessageID(msg_id) + ]) ) else: result = self._client( - functions.messages.GetMessagesRequest([msg_id]) + functions.messages.GetMessagesRequest([ + types.InputMessageID(msg_id) + ]) ) except RPCError: return None, None @@ -500,11 +504,13 @@ class NewMessage(_EventBuilder): if self._reply_message is None: if isinstance(self.input_chat, types.InputPeerChannel): r = self._client(functions.channels.GetMessagesRequest( - self.input_chat, [self.message.reply_to_msg_id] + self.input_chat, [ + types.InputMessageID(self.message.reply_to_msg_id) + ] )) else: r = self._client(functions.messages.GetMessagesRequest( - [self.message.reply_to_msg_id] + [types.InputMessageID(self.message.reply_to_msg_id)] )) if not isinstance(r, types.messages.MessagesNotModified): self._reply_message = r.messages[0] @@ -817,7 +823,9 @@ class ChatAction(_EventBuilder): if isinstance(self._pinned_message, int) and self.input_chat: r = self._client(functions.channels.GetMessagesRequest( - self._input_chat, [self._pinned_message] + self._input_chat, [ + types.InputMessageID(self._pinned_message) + ] )) try: self._pinned_message = next( @@ -1221,14 +1229,16 @@ class MessageRead(_EventBuilder): if not chat: self._messages = [] elif isinstance(chat, types.InputPeerChannel): + ids = [types.InputMessageID(x) for x in self._message_ids] self._messages =\ self._client(functions.channels.GetMessagesRequest( - chat, self._message_ids + chat, ids )).messages else: + ids = [types.InputMessageID(x) for x in self._message_ids] self._messages =\ self._client(functions.messages.GetMessagesRequest( - self._message_ids + ids )).messages return self._messages diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index a736b066..2ed348da 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -556,7 +556,7 @@ accountDaysTTL#b8d0afdf days:int = AccountDaysTTL; documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; documentAttributeAnimated#11b58939 = DocumentAttribute; documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute; -documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true duration:int w:int h:int = DocumentAttribute; +documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true supports_streaming:flags.1?true duration:int w:int h:int = DocumentAttribute; documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute; documentAttributeFilename#15590068 file_name:string = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute; @@ -938,7 +938,15 @@ recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; -inputSingleMedia#31bc3d25 media:InputMedia flags:# random_id:long message:string entities:flags.0?Vector = InputSingleMedia; +inputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector = InputSingleMedia; + +webAuthorization#cac943f2 hash:long bot_id:int domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization; + +account.webAuthorizations#ed56c9fc authorizations:Vector users:Vector = account.WebAuthorizations; + +inputMessageID#a676a322 id:int = InputMessage; +inputMessageReplyTo#bad88395 id:int = InputMessage; +inputMessagePinned#86872538 = InputMessage; ---functions--- @@ -993,6 +1001,9 @@ account.updatePasswordSettings#fa7c4b86 current_password_hash:bytes new_settings account.sendConfirmPhoneCode#1516d7bd flags:# allow_flashcall:flags.0?true hash:string current_number:flags.0?Bool = auth.SentCode; account.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool; account.getTmpPassword#4a82327e password_hash:bytes period:int = account.TmpPassword; +account.getWebAuthorizations#182e6d6f = account.WebAuthorizations; +account.resetWebAuthorization#2d01b9ef hash:long = Bool; +account.resetWebAuthorizations#682d2594 = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#ca30a5b1 id:InputUser = UserFull; @@ -1013,7 +1024,7 @@ contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags. contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; contacts.resetSaved#879537f1 = Bool; -messages.getMessages#4222fa74 id:Vector = messages.Messages; +messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs; messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages; messages.search#39e9ea0 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; @@ -1141,7 +1152,7 @@ channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory; channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector = Bool; -channels.getMessages#93d7b347 channel:InputChannel id:Vector = messages.Messages; +channels.getMessages#ad8c9a23 channel:InputChannel id:Vector = messages.Messages; channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants; channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant; channels.getChannels#a7f6bbb id:Vector = messages.Chats; From cb226e7f4571b4af12f42ad97cc2b8cb43aade8e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 1 Apr 2018 12:46:44 +0200 Subject: [PATCH 361/361] Revisit telethon_generator (closes #724) --- telethon_generator/parser/source_builder.py | 11 +- telethon_generator/tl_generator.py | 265 +++++++++----------- 2 files changed, 130 insertions(+), 146 deletions(-) diff --git a/telethon_generator/parser/source_builder.py b/telethon_generator/parser/source_builder.py index 2b62cf61..9fb61593 100644 --- a/telethon_generator/parser/source_builder.py +++ b/telethon_generator/parser/source_builder.py @@ -16,7 +16,7 @@ class SourceBuilder: """ self.write(' ' * (self.current_indent * self.indent_size)) - def write(self, string): + def write(self, string, *args, **kwargs): """Writes a string into the source code, applying indentation if required """ @@ -26,13 +26,16 @@ class SourceBuilder: if string.strip(): self.indent() - self.out_stream.write(string) + if args or kwargs: + self.out_stream.write(string.format(*args, **kwargs)) + else: + self.out_stream.write(string) - def writeln(self, string=''): + def writeln(self, string='', *args, **kwargs): """Writes a string into the source code _and_ appends a new line, applying indentation if required """ - self.write(string + '\n') + self.write(string + '\n', *args, **kwargs) self.on_new_line = True # If we're writing a block, increment indent for the next time diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 7c1f6237..85ee98a0 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -24,9 +24,11 @@ class TLGenerator: self.output_dir = output_dir def _get_file(self, *paths): + """Wrapper around ``os.path.join()`` with output as first path.""" return os.path.join(self.output_dir, *paths) def _rm_if_exists(self, filename): + """Recursively deletes the given filename if it exists.""" file = self._get_file(filename) if os.path.exists(file): if os.path.isdir(file): @@ -35,19 +37,21 @@ class TLGenerator: os.remove(file) def tlobjects_exist(self): - """Determines whether the TLObjects were previously - generated (hence exist) or not + """ + Determines whether the TLObjects were previously + generated (hence exist) or not. """ return os.path.isfile(self._get_file('all_tlobjects.py')) def clean_tlobjects(self): - """Cleans the automatically generated TLObjects from disk""" + """Cleans the automatically generated TLObjects from disk.""" for name in ('functions', 'types', 'all_tlobjects.py'): self._rm_if_exists(name) def generate_tlobjects(self, scheme_file, import_depth): - """Generates all the TLObjects from scheme.tl to - tl/functions and tl/types + """ + Generates all the TLObjects from the ``scheme_file`` to + ``tl/functions`` and ``tl/types``. """ # First ensure that the required parent directories exist @@ -85,42 +89,33 @@ class TLGenerator: # Step 4: Once all the objects have been generated, # we can now group them in a single file filename = os.path.join(self._get_file('all_tlobjects.py')) - with open(filename, 'w', encoding='utf-8') as file: - with SourceBuilder(file) as builder: - builder.writeln(AUTO_GEN_NOTICE) - builder.writeln() + with open(filename, 'w', encoding='utf-8') as file,\ + SourceBuilder(file) as builder: + builder.writeln(AUTO_GEN_NOTICE) + builder.writeln() - builder.writeln('from . import types, functions') - builder.writeln() + builder.writeln('from . import types, functions') + builder.writeln() - # Create a constant variable to indicate which layer this is - builder.writeln('LAYER = {}'.format( - TLParser.find_layer(scheme_file)) - ) - builder.writeln() + # Create a constant variable to indicate which layer this is + builder.writeln('LAYER = {}', TLParser.find_layer(scheme_file)) + builder.writeln() - # Then create the dictionary containing constructor_id: class - builder.writeln('tlobjects = {') - builder.current_indent += 1 + # Then create the dictionary containing constructor_id: class + builder.writeln('tlobjects = {') + builder.current_indent += 1 - # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) - for tlobject in tlobjects: - constructor = hex(tlobject.id) - if len(constructor) != 10: - # Make it a nice length 10 so it fits well - constructor = '0x' + constructor[2:].zfill(8) + # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) + for tlobject in tlobjects: + builder.write('{:#010x}: ', tlobject.id) + builder.write('functions' if tlobject.is_function else 'types') + if tlobject.namespace: + builder.write('.' + tlobject.namespace) - builder.write('{}: '.format(constructor)) - builder.write( - 'functions' if tlobject.is_function else 'types') + builder.writeln('.{},', tlobject.class_name()) - if tlobject.namespace: - builder.write('.' + tlobject.namespace) - - builder.writeln('.{},'.format(tlobject.class_name())) - - builder.current_indent -= 1 - builder.writeln('}') + builder.current_indent -= 1 + builder.writeln('}') @staticmethod def _write_init_py(out_dir, depth, namespace_tlobjects, type_constructors): @@ -136,16 +131,17 @@ class TLGenerator: # so they all can be serialized and sent, however, only the # functions are "content_related". builder.writeln( - 'from {}.tl.tlobject import TLObject'.format('.' * depth) + 'from {}.tl.tlobject import TLObject', '.' * depth ) - builder.writeln('from typing import Optional, List, Union, TYPE_CHECKING') + builder.writeln('from typing import Optional, List, ' + 'Union, TYPE_CHECKING') # Add the relative imports to the namespaces, # unless we already are in a namespace. if not ns: - builder.writeln('from . import {}'.format(', '.join( + builder.writeln('from . import {}', ', '.join( x for x in namespace_tlobjects.keys() if x - ))) + )) # Import 'os' for those needing access to 'os.urandom()' # Currently only 'random_id' needs 'os' to be imported, @@ -204,18 +200,18 @@ class TLGenerator: if name == 'date': imports['datetime'] = ['datetime'] continue - elif not import_space in imports: + elif import_space not in imports: imports[import_space] = set() imports[import_space].add('Type{}'.format(name)) - # Add imports required for type checking. - builder.writeln('if TYPE_CHECKING:') - for namespace, names in imports.items(): - builder.writeln('from {} import {}'.format( - namespace, ', '.join(names))) - else: - builder.writeln('pass') - builder.end_block() + # Add imports required for type checking + if imports: + builder.writeln('if TYPE_CHECKING:') + for namespace, names in imports.items(): + builder.writeln('from {} import {}', + namespace, ', '.join(names)) + + builder.end_block() # Generate the class for every TLObject for t in tlobjects: @@ -229,25 +225,24 @@ class TLGenerator: for line in type_defs: builder.writeln(line) - @staticmethod def _write_source_code(tlobject, builder, depth, type_constructors): - """Writes the source code corresponding to the given TLObject - by making use of the 'builder' SourceBuilder. + """ + Writes the source code corresponding to the given TLObject + by making use of the ``builder`` `SourceBuilder`. - Additional information such as file path depth and - the Type: [Constructors] must be given for proper - importing and documentation strings. + Additional information such as file path depth and + the ``Type: [Constructors]`` must be given for proper + importing and documentation strings. """ builder.writeln() builder.writeln() - builder.writeln('class {}(TLObject):'.format(tlobject.class_name())) + builder.writeln('class {}(TLObject):', tlobject.class_name()) # Class-level variable to store its Telegram's constructor ID - builder.writeln('CONSTRUCTOR_ID = {}'.format(hex(tlobject.id))) - builder.writeln('SUBCLASS_OF_ID = {}'.format( - hex(crc32(tlobject.result.encode('ascii')))) - ) + builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id) + builder.writeln('SUBCLASS_OF_ID = {:#x}', + crc32(tlobject.result.encode('ascii'))) builder.writeln() # Flag arguments must go last @@ -265,9 +260,7 @@ class TLGenerator: # Write the __init__ function if args: - builder.writeln( - 'def __init__(self, {}):'.format(', '.join(args)) - ) + builder.writeln('def __init__(self, {}):', ', '.join(args)) else: builder.writeln('def __init__(self):') @@ -286,30 +279,27 @@ class TLGenerator: builder.writeln('"""') for arg in args: if not arg.flag_indicator: - builder.writeln(':param {} {}:'.format( - arg.doc_type_hint(), arg.name - )) + builder.writeln(':param {} {}:', + arg.doc_type_hint(), arg.name) builder.current_indent -= 1 # It will auto-indent (':') # We also want to know what type this request returns # or to which type this constructor belongs to builder.writeln() if tlobject.is_function: - builder.write(':returns {}: '.format(tlobject.result)) + builder.write(':returns {}: ', tlobject.result) else: - builder.write('Constructor for {}: '.format(tlobject.result)) + builder.write('Constructor for {}: ', tlobject.result) constructors = type_constructors[tlobject.result] if not constructors: builder.writeln('This type has no constructors.') elif len(constructors) == 1: - builder.writeln('Instance of {}.'.format( - constructors[0].class_name() - )) + builder.writeln('Instance of {}.', + constructors[0].class_name()) else: - builder.writeln('Instance of either {}.'.format( - ', '.join(c.class_name() for c in constructors) - )) + builder.writeln('Instance of either {}.', ', '.join( + c.class_name() for c in constructors)) builder.writeln('"""') @@ -327,8 +317,8 @@ class TLGenerator: for arg in args: if not arg.can_be_inferred: - builder.writeln('self.{0} = {0} # type: {1}'.format( - arg.name, arg.python_type_hint())) + builder.writeln('self.{0} = {0} # type: {1}', + arg.name, arg.python_type_hint()) continue # Currently the only argument that can be @@ -350,7 +340,7 @@ class TLGenerator: builder.writeln( "self.random_id = random_id if random_id " - "is not None else {}".format(code) + "is not None else {}", code ) else: raise ValueError('Cannot infer a value for ', arg) @@ -374,27 +364,27 @@ class TLGenerator: base_types = ('string', 'bytes', 'int', 'long', 'int128', 'int256', 'double', 'Bool', 'true', 'date') - builder.write("'_': '{}'".format(tlobject.class_name())) + builder.write("'_': '{}'", tlobject.class_name()) for arg in args: builder.writeln(',') - builder.write("'{}': ".format(arg.name)) + builder.write("'{}': ", arg.name) if arg.type in base_types: if arg.is_vector: - builder.write('[] if self.{0} is None else self.{0}[:]' - .format(arg.name)) + builder.write('[] if self.{0} is None else self.{0}[:]', + arg.name) else: - builder.write('self.{}'.format(arg.name)) + builder.write('self.{}', arg.name) else: if arg.is_vector: builder.write( '[] if self.{0} is None else [None ' - 'if x is None else x.to_dict() for x in self.{0}]' - .format(arg.name) + 'if x is None else x.to_dict() for x in self.{0}]', + arg.name ) else: builder.write( - 'None if self.{0} is None else self.{0}.to_dict()' - .format(arg.name) + 'None if self.{0} is None else self.{0}.to_dict()', + arg.name ) builder.writeln() @@ -421,17 +411,16 @@ class TLGenerator: .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( - ' and '.join(cnd1), ' and '.join(cnd2), - ', '.join(a.name for a in ra) - ) + "be False-y (like None) or all me True-y'", + ' and '.join(cnd1), ' and '.join(cnd2), + ', '.join(a.name for a in ra) ) builder.writeln("return b''.join((") builder.current_indent += 1 # First constructor code, we already know its bytes - builder.writeln('{},'.format(repr(struct.pack('3.5 feature, so add another join. @@ -560,7 +551,7 @@ class TLGenerator: arg.is_vector = True arg.is_flag = old_flag - builder.write(' for x in {})'.format(name)) + builder.write(' for x in {})', name) elif arg.flag_indicator: # Calculate the flags with those items which are not None @@ -579,41 +570,39 @@ class TLGenerator: elif 'int' == arg.type: # struct.pack is around 4 times faster than int.to_bytes - builder.write("struct.pack('