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
-
Although this documentation was generated for Telethon, it may
- be useful for any other Telegram library out there.
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:
-
-
How to create a TelegramClient.
-
Connecting to the Telegram servers and authorizing an user.
-
Retrieving a list of chats (dialogs).
-
Invoking a request without the built-in methods.
-
-
#!/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.
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…" />
+
+ Exact match:
+
+
+
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('