Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Christian Stemmle 2017-08-17 15:58:01 +02:00
commit 34261f1f76
26 changed files with 1178 additions and 639 deletions

View File

@ -58,11 +58,11 @@ On a terminal, issue the following command:
sudo -H pip install telethon sudo -H pip install telethon
You're ready to go. Oh, and upgrading is just as easy: If you get something like "SyntaxError: invalid syntax" on the ``from error``
line, it's because ``pip`` defaults to Python 2. Use `pip3` instead.
.. code:: sh If you already have Telethon installed,
upgrade with ``pip install --upgrade telethon``!
sudo -H pip install --upgrade telethon
Installing Telethon manually Installing Telethon manually
---------------------------- ----------------------------
@ -71,7 +71,7 @@ Installing Telethon manually
(`GitHub <https://github.com/ricmoo/pyaes>`_, `package index <https://pypi.python.org/pypi/pyaes>`_) (`GitHub <https://github.com/ricmoo/pyaes>`_, `package index <https://pypi.python.org/pypi/pyaes>`_)
2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git`` 2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git``
3. Enter the cloned repository: ``cd Telethon`` 3. Enter the cloned repository: ``cd Telethon``
4. Run the code generator: ``cd telethon_generator && python3 tl_generator.py`` 4. Run the code generator: ``python3 setup.py gen_tl``
5. Done! 5. Done!
Running Telethon Running Telethon
@ -160,13 +160,16 @@ The ``TelegramClient`` class should be used to provide a quick, well-documented
It is **not** meant to be a place for *all* the available Telegram ``Request``'s, because there are simply too many. It is **not** meant to be a place for *all* the available Telegram ``Request``'s, because there are simply too many.
However, this doesn't mean that you cannot ``invoke`` all the power of Telegram's API. However, this doesn't mean that you cannot ``invoke`` all the power of Telegram's API.
Whenever you need to ``invoke`` a Telegram ``Request``, all you need to do is the following: Whenever you need to ``call`` a Telegram ``Request``, all you need to do is the following:
.. code:: python .. code:: python
result = client(SomeRequest(...))
# Or the old way:
result = client.invoke(SomeRequest(...)) result = client.invoke(SomeRequest(...))
You have just ``invoke``'d ``SomeRequest`` and retrieved its ``result``! That wasn't hard at all, was it? You have just called ``SomeRequest`` and retrieved its ``result``! That wasn't hard at all, was it?
Now you may wonder, what's the deal with *all the power of Telegram's API*? Have a look under ``tl/functions/``. Now you may wonder, what's the deal with *all the power of Telegram's API*? Have a look under ``tl/functions/``.
That is *everything* you can do. You have **over 200 API** ``Request``'s at your disposal. That is *everything* you can do. You have **over 200 API** ``Request``'s at your disposal.
@ -232,7 +235,7 @@ Have you found a more updated version of the ``scheme.tl`` file? Those are great
as grabbing the as grabbing the
`latest version <https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/scheme.tl>`_ `latest version <https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/scheme.tl>`_
and replacing the one you can find in this same directory by the updated one. and replacing the one you can find in this same directory by the updated one.
Don't forget to run ``python3 tl_generator.py``. Don't forget to run ``python3 setup.py gen_tl``.
If the changes weren't too big, everything should still work the same way as it did before; but with extra features. If the changes weren't too big, everything should still work the same way as it did before; but with extra features.

View File

@ -75,12 +75,19 @@ def get_create_path_for(tlobject):
return os.path.join(out_dir, get_file_name(tlobject, add_extension=True)) return os.path.join(out_dir, get_file_name(tlobject, add_extension=True))
def is_core_type(type_):
"""Returns "true" if the type is considered a core type"""
return type_.lower() in {
'int', 'long', 'int128', 'int256', 'double',
'vector', 'string', 'bool', 'true', 'bytes', 'date'
}
def get_path_for_type(type_, relative_to='.'): def get_path_for_type(type_, relative_to='.'):
"""Similar to getting the path for a TLObject, it might not be possible """Similar to getting the path for a TLObject, it might not be possible
to have the TLObject itself but rather its name (the type); to have the TLObject itself but rather its name (the type);
this method works in the same way, returning a relative path""" this method works in the same way, returning a relative path"""
if type_.lower() in {'int', 'long', 'int128', 'int256', 'double', if is_core_type(type_):
'vector', 'string', 'bool', 'true', 'bytes', 'date'}:
path = 'index.html#%s' % type_.lower() path = 'index.html#%s' % type_.lower()
elif '.' in type_: elif '.' in type_:
@ -396,11 +403,39 @@ def generate_documentation(scheme_file):
docs.add_row(get_class_name(func), link=link) docs.add_row(get_class_name(func), link=link)
docs.end_table() docs.end_table()
# List all the methods which take this type as input
docs.write_title('Methods accepting this type as input', level=3)
other_methods = sorted(
(t for t in tlobjects
if any(tltype == a.type for a in t.args) and t.is_function),
key=lambda t: t.name
)
if not other_methods:
docs.write_text(
'No methods accept this type as an input parameter.')
elif len(other_methods) == 1:
docs.write_text(
'Only this method has a parameter with this type.')
else:
docs.write_text(
'The following %d methods accept this type as an input '
'parameter.' % len(other_methods))
docs.begin_table(2)
for ot in other_methods:
link = get_create_path_for(ot)
link = get_relative_path(link, relative_to=filename)
docs.add_row(get_class_name(ot), link=link)
docs.end_table()
# List every other type which has this type as a member # List every other type which has this type as a member
docs.write_title('Other types containing this type', level=3) docs.write_title('Other types containing this type', level=3)
other_types = sorted((t for t in tlobjects other_types = sorted(
if any(tltype == a.type for a in t.args)), (t for t in tlobjects
key=lambda t: t.name) if any(tltype == a.type for a in t.args)
and not t.is_function
), key=lambda t: t.name
)
if not other_types: if not other_types:
docs.write_text( docs.write_text(
@ -433,20 +468,30 @@ def generate_documentation(scheme_file):
layer = TLParser.find_layer(scheme_file) layer = TLParser.find_layer(scheme_file)
types = set() types = set()
methods = [] methods = []
constructors = []
for tlobject in tlobjects: for tlobject in tlobjects:
if tlobject.is_function: if tlobject.is_function:
methods.append(tlobject) methods.append(tlobject)
else:
constructors.append(tlobject)
types.add(tlobject.result) if not is_core_type(tlobject.result):
if re.search('^vector<', tlobject.result, re.IGNORECASE):
types.add(tlobject.result.split('<')[1].strip('>'))
else:
types.add(tlobject.result)
types = sorted(types) types = sorted(types)
methods = sorted(methods, key=lambda m: m.name) methods = sorted(methods, key=lambda m: m.name)
constructors = sorted(constructors, key=lambda c: c.name)
request_names = ', '.join('"' + get_class_name(m) + '"' for m in methods) request_names = ', '.join('"' + get_class_name(m) + '"' for m in methods)
type_names = ', '.join('"' + get_class_name(t) + '"' for t in types) type_names = ', '.join('"' + get_class_name(t) + '"' for t in types)
constructor_names = ', '.join('"' + get_class_name(t) + '"' for t in constructors)
request_urls = ', '.join('"' + get_create_path_for(m) + '"' for m in methods) request_urls = ', '.join('"' + get_create_path_for(m) + '"' for m in methods)
type_urls = ', '.join('"' + get_path_for_type(t) + '"' for t in types) type_urls = ', '.join('"' + get_path_for_type(t) + '"' for t in types)
constructor_urls = ', '.join('"' + get_create_path_for(t) + '"' for t in constructors)
replace_dict = { replace_dict = {
'type_count': len(types), 'type_count': len(types),
@ -456,8 +501,10 @@ def generate_documentation(scheme_file):
'request_names': request_names, 'request_names': request_names,
'type_names': type_names, 'type_names': type_names,
'constructor_names': constructor_names,
'request_urls': request_urls, 'request_urls': request_urls,
'type_urls': type_urls 'type_urls': type_urls,
'constructor_urls': constructor_urls
} }
with open('../res/core.html') as infile: with open('../res/core.html') as infile:

View File

@ -14,11 +14,26 @@
</head> </head>
<body> <body>
<div id="main_div"> <div id="main_div">
<!-- You can append '?q=query' to the URL to default to a search -->
<input id="searchBox" type="text" onkeyup="updateSearch()" <input id="searchBox" type="text" onkeyup="updateSearch()"
placeholder="Search for requests and types…" /> placeholder="Search for requests and types…" />
<div id="searchDiv"> <div id="searchDiv">
<table id="searchTable"></table>
<details open><summary class="title">Methods (<span id="methodsCount">0</span>)</summary>
<ul id="methodsList" class="together">
</ul>
</details>
<details open><summary class="title">Types (<span id="typesCount">0</span>)</summary>
<ul id="typesList" class="together">
</ul>
</details>
<details><summary class="title">Constructors (<span id="constructorsCount">0</span>)</summary>
<ul id="constructorsList" class="together">
</ul>
</details>
</div> </div>
<div id="contentDiv"> <div id="contentDiv">
@ -146,7 +161,6 @@
<pre><span class="sh3">#!/usr/bin/python3</span> <pre><span class="sh3">#!/usr/bin/python3</span>
<span class="sh4">from</span> telethon <span class="sh4">import</span> TelegramClient <span class="sh4">from</span> telethon <span class="sh4">import</span> TelegramClient
<span class="sh4">from</span> telethon.tl.functions.messages <span class="sh4">import</span> GetHistoryRequest <span class="sh4">from</span> telethon.tl.functions.messages <span class="sh4">import</span> GetHistoryRequest
<span class="sh4">from</span> telethon.utils <span class="sh4">import</span> get_input_peer
<span class="sh3"># <b>(1)</b> Use your own values here</span> <span class="sh3"># <b>(1)</b> Use your own values here</span>
api_id = <span class="sh1">12345</span> api_id = <span class="sh1">12345</span>
@ -167,25 +181,28 @@ dialogs, entities = client.get_dialogs(<span class="sh1">10</span>)
entity = entities[<span class="sh1">0</span>] entity = entities[<span class="sh1">0</span>]
<span class="sh3"># <b>(4)</b> !! Invoking a request manually !!</span> <span class="sh3"># <b>(4)</b> !! Invoking a request manually !!</span>
result = <b>client.invoke</b>( result = <b>client</b>(GetHistoryRequest(
GetHistoryRequest( entity,
get_input_peer(entity), limit=<span class="sh1">20</span>,
limit=<span class="sh1">20</span>, offset_date=<span class="sh1">None</span>,
offset_date=<span class="sh1">None</span>, offset_id=<span class="sh1">0</span>,
offset_id=<span class="sh1">0</span>, max_id=<span class="sh1">0</span>,
max_id=<span class="sh1">0</span>, min_id=<span class="sh1">0</span>,
min_id=<span class="sh1">0</span>, add_offset=<span class="sh1">0</span>
add_offset=<span class="sh1">0</span>)) ))
<span class="sh3"># Now you have access to the first 20 messages</span> <span class="sh3"># Now you have access to the first 20 messages</span>
messages = result.messages</pre> messages = result.messages</pre>
<p>As it can be seen, manually invoking requests with <p>As it can be seen, manually calling requests with
<code>client.invoke()</code> is way more verbose than using the built-in <code>client(request)</code> (or using the old way, by calling
methods (such as <code>client.get_dialogs()</code>. However, and given <code>client.invoke(request)</code>) is way more verbose than using the
that there are so many methods available, it's impossible to provide a nice built-in methods (such as <code>client.get_dialogs()</code>).</p>
interface to things that may change over time. To get full access, however,
you're still able to invoke these methods manually.</p> <p>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.</p>
</div> </div>
</div> </div>
@ -193,13 +210,66 @@ messages = result.messages</pre>
contentDiv = document.getElementById("contentDiv"); contentDiv = document.getElementById("contentDiv");
searchDiv = document.getElementById("searchDiv"); searchDiv = document.getElementById("searchDiv");
searchBox = document.getElementById("searchBox"); searchBox = document.getElementById("searchBox");
searchTable = document.getElementById("searchTable");
requests = [{request_names}]; // Search lists
types = [{type_names}]; methodsList = document.getElementById("methodsList");
methodsCount = document.getElementById("methodsCount");
requestsu = [{request_urls}]; typesList = document.getElementById("typesList");
typesu = [{type_urls}]; typesCount = document.getElementById("typesCount");
constructorsList = document.getElementById("constructorsList");
constructorsCount = document.getElementById("constructorsCount");
try {
requests = [{request_names}];
types = [{type_names}];
constructors = [{constructor_names}];
requestsu = [{request_urls}];
typesu = [{type_urls}];
constructorsu = [{constructor_urls}];
} catch (e) {
requests = [];
types = [];
constructors = [];
requestsu = [];
typesu = [];
constructorsu = [];
}
// Given two input arrays "original" and "original urls" and a query,
// return a pair of arrays with matching "query" elements from "original".
//
// TODO Perhaps return an array of pairs instead a pair of arrays (for cache).
function getSearchArray(original, originalu, query) {
var destination = [];
var destinationu = [];
for (var i = 0; i < original.length; ++i) {
if (original[i].toLowerCase().indexOf(query) != -1) {
destination.push(original[i]);
destinationu.push(originalu[i]);
}
}
return [destination, destinationu];
}
// Modify "countSpan" and "resultList" accordingly based on the elements
// given as [[elements], [element urls]] (both with the same length)
function buildList(countSpan, resultList, foundElements) {
var result = "";
for (var i = 0; i < foundElements[0].length; ++i) {
result += '<li>';
result += '<a href="' + foundElements[1][i] + '">';
result += foundElements[0][i];
result += '</a></li>';
}
countSpan.innerHTML = "" + foundElements[0].length;
resultList.innerHTML = result;
}
function updateSearch() { function updateSearch() {
if (searchBox.value) { if (searchBox.value) {
@ -207,52 +277,37 @@ function updateSearch() {
searchDiv.style.display = ""; searchDiv.style.display = "";
var query = searchBox.value.toLowerCase(); var query = searchBox.value.toLowerCase();
var foundRequests = [];
var foundRequestsu = [];
for (var i = 0; i < requests.length; ++i) {
if (requests[i].toLowerCase().indexOf(query) != -1) {
foundRequests.push(requests[i]);
foundRequestsu.push(requestsu[i]);
}
}
var foundTypes = []; var foundRequests = getSearchArray(requests, requestsu, query);
var foundTypesu = []; var foundTypes = getSearchArray(types, typesu, query);
for (var i = 0; i < types.length; ++i) { var foundConstructors = getSearchArray(
if (types[i].toLowerCase().indexOf(query) != -1) { constructors, constructorsu, query
foundTypes.push(types[i]); );
foundTypesu.push(typesu[i]);
}
}
var top = foundRequests.length > foundTypes.length ? buildList(methodsCount, methodsList, foundRequests);
foundRequests.length : foundTypes.length; buildList(typesCount, typesList, foundTypes);
buildList(constructorsCount, constructorsList, foundConstructors);
result = "";
for (var i = 0; i <= top; ++i) {
result += "<tr><td>";
if (i < foundRequests.length) {
result +=
'<a href="'+foundRequestsu[i]+'">'+foundRequests[i]+'</a>';
}
result += "</td><td>";
if (i < foundTypes.length) {
result +=
'<a href="'+foundTypesu[i]+'">'+foundTypes[i]+'</a>';
}
result += "</td></tr>";
}
searchTable.innerHTML = result;
} else { } else {
contentDiv.style.display = ""; contentDiv.style.display = "";
searchDiv.style.display = "none"; searchDiv.style.display = "none";
} }
} }
function getQuery(name) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i != vars.length; ++i) {
var pair = vars[i].split("=");
if (pair[0] == name)
return pair[1];
}
}
var query = getQuery('q');
if (query) {
searchBox.value = query;
}
updateSearch(); updateSearch();
</script> </script>
</body> </body>

View File

@ -52,7 +52,7 @@ table td {
margin: 0 8px -2px 0; margin: 0 8px -2px 0;
} }
h1 { h1, summary.title {
font-size: 24px; font-size: 24px;
} }
@ -137,6 +137,26 @@ button:hover {
color: #fff; color: #fff;
} }
/* https://www.w3schools.com/css/css_navbar.asp */
ul.together {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
}
ul.together li {
float: left;
}
ul.together li a {
display: block;
border-radius: 8px;
background: #f0f4f8;
padding: 4px 8px;
margin: 8px;
}
/* https://stackoverflow.com/a/30810322 */ /* https://stackoverflow.com/a/30810322 */
.invisible { .invisible {
left: 0; left: 0;
@ -153,7 +173,7 @@ button:hover {
} }
@media (max-width: 640px) { @media (max-width: 640px) {
h1 { h1, summary.title {
font-size: 18px; font-size: 18px;
} }
h3 { h3 {

127
setup.py Normal file → Executable file
View File

@ -1,87 +1,98 @@
#!/usr/bin/env python3
"""A setuptools based setup module. """A setuptools based setup module.
See: See:
https://packaging.python.org/en/latest/distributing.html https://packaging.python.org/en/latest/distributing.html
https://github.com/pypa/sampleproject https://github.com/pypa/sampleproject
Extra supported commands are:
* gen_tl, to generate the classes required for Telethon to run
* clean_tl, to clean these generated classes
""" """
# To use a consistent encoding # To use a consistent encoding
from codecs import open from codecs import open
from sys import argv
from os import path from os import path
# Always prefer setuptools over distutils # Always prefer setuptools over distutils
from setuptools import find_packages, setup from setuptools import find_packages, setup
from telethon import TelegramClient try:
from telethon import TelegramClient
except ImportError:
TelegramClient = None
here = path.abspath(path.dirname(__file__))
# Get the long description from the README file if __name__ == '__main__':
with open(path.join(here, 'README.rst'), encoding='utf-8') as f: if len(argv) >= 2 and argv[1] == 'gen_tl':
long_description = f.read() from telethon_generator.tl_generator import TLGenerator
generator = TLGenerator('telethon/tl')
if generator.tlobjects_exist():
print('Detected previous TLObjects. Cleaning...')
generator.clean_tlobjects()
setup( print('Generating TLObjects...')
name='Telethon', generator.generate_tlobjects(
'telethon_generator/scheme.tl', import_depth=2
)
print('Done.')
# Versions should comply with PEP440. elif len(argv) >= 2 and argv[1] == 'clean_tl':
version=TelegramClient.__version__, from telethon_generator.tl_generator import TLGenerator
description="Python3 Telegram's client implementation with full access to its API", print('Cleaning...')
long_description=long_description, TLGenerator('telethon/tl').clean_tlobjects()
print('Done.')
# The project's main homepage. else:
url='https://github.com/LonamiWebs/Telethon', if not TelegramClient:
download_url='https://github.com/LonamiWebs/Telethon/releases', print('Run `python3', argv[0], 'gen_tl` first.')
quit()
# Author details here = path.abspath(path.dirname(__file__))
author='Lonami Exo',
author_email='totufals@hotmail.com',
# Choose your license # Get the long description from the README file
license='MIT', with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read()
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers setup(
classifiers=[ name='Telethon',
# How mature is this project? Common values are
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
'Development Status :: 3 - Alpha',
# Indicate who your project is intended for # Versions should comply with PEP440.
'Intended Audience :: Developers', version=TelegramClient.__version__,
'Topic :: Communications :: Chat', description="Full-featured Telegram client library for Python 3",
long_description=long_description,
# Pick your license as you wish (should match "license" above) url='https://github.com/LonamiWebs/Telethon',
'License :: OSI Approved :: MIT License', download_url='https://github.com/LonamiWebs/Telethon/releases',
# Specify the Python versions you support here. In particular, ensure author='Lonami Exo',
# that you indicate whether you support Python 2, Python 3 or both. author_email='totufals@hotmail.com',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6'
],
# What does your project relate to? license='MIT',
keywords='Telegram API chat client MTProto',
# You can just specify the packages manually here if your project is # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
# simple. Or you can use find_packages(). classifiers=[
packages=find_packages(exclude=[ # 3 - Alpha
'telethon_generator', 'telethon_tests', 'run_tests.py', # 4 - Beta
'try_telethon.py' # 5 - Production/Stable
]), 'Development Status :: 3 - Alpha',
# List run-time dependencies here. These will be installed by pip when 'Intended Audience :: Developers',
# your project is installed. 'Topic :: Communications :: Chat',
install_requires=['pyaes'],
# To provide executable scripts, use entry points in preference to the 'License :: OSI Approved :: MIT License',
# "scripts" keyword. Entry points provide cross-platform support and allow
# pip to create the appropriate form of executable for the target platform. 'Programming Language :: Python :: 3',
entry_points={ 'Programming Language :: Python :: 3.3',
'console_scripts': [ 'Programming Language :: Python :: 3.4',
'gen_tl = tl_generator:clean_and_generate', 'Programming Language :: Python :: 3.5',
], 'Programming Language :: Python :: 3.6'
}) ],
keywords='telegram api chat client library messaging mtproto',
packages=find_packages(exclude=[
'telethon_generator', 'telethon_tests', 'run_tests.py',
'try_telethon.py'
]),
install_requires=['pyaes']
)

View File

@ -18,10 +18,10 @@ from .rpc_errors_420 import *
def rpc_message_to_error(code, message): def rpc_message_to_error(code, message):
errors = { errors = {
303: rpc_303_errors, 303: rpc_errors_303_all,
400: rpc_400_errors, 400: rpc_errors_400_all,
401: rpc_401_errors, 401: rpc_errors_401_all,
420: rpc_420_errors 420: rpc_errors_420_all
}.get(code, None) }.get(code, None)
if errors is not None: if errors is not None:

View File

@ -43,7 +43,7 @@ class UserMigrateError(InvalidDCError):
) )
rpc_303_errors = { rpc_errors_303_all = {
'FILE_MIGRATE_(\d+)': FileMigrateError, 'FILE_MIGRATE_(\d+)': FileMigrateError,
'PHONE_MIGRATE_(\d+)': PhoneMigrateError, 'PHONE_MIGRATE_(\d+)': PhoneMigrateError,
'NETWORK_MIGRATE_(\d+)': NetworkMigrateError, 'NETWORK_MIGRATE_(\d+)': NetworkMigrateError,

View File

@ -44,6 +44,14 @@ class ChatIdInvalidError(BadRequestError):
) )
class ConnectionLangPackInvalid(BadRequestError):
def __init__(self, **kwargs):
super(Exception, self).__init__(
self,
'The specified language pack is not valid.'
)
class ConnectionLayerInvalidError(BadRequestError): class ConnectionLayerInvalidError(BadRequestError):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Exception, self).__init__( super(Exception, self).__init__(
@ -321,7 +329,7 @@ class UserIdInvalidError(BadRequestError):
) )
rpc_400_errors = { rpc_errors_400_all = {
'API_ID_INVALID': ApiIdInvalidError, 'API_ID_INVALID': ApiIdInvalidError,
'BOT_METHOD_INVALID': BotMethodInvalidError, 'BOT_METHOD_INVALID': BotMethodInvalidError,
'CHANNEL_INVALID': ChannelInvalidError, 'CHANNEL_INVALID': ChannelInvalidError,

View File

@ -84,7 +84,7 @@ class UserDeactivatedError(UnauthorizedError):
) )
rpc_401_errors = { rpc_errors_401_all = {
'ACTIVE_USER_REQUIRED': ActiveUserRequiredError, 'ACTIVE_USER_REQUIRED': ActiveUserRequiredError,
'AUTH_KEY_INVALID': AuthKeyInvalidError, 'AUTH_KEY_INVALID': AuthKeyInvalidError,
'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError, 'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError,

View File

@ -11,6 +11,6 @@ class FloodWaitError(FloodError):
) )
rpc_420_errors = { rpc_errors_420_all = {
'FLOOD_WAIT_(\d+)': FloodWaitError 'FLOOD_WAIT_(\d+)': FloodWaitError
} }

View File

@ -30,9 +30,12 @@ class TcpClient:
else: # tuple, list, etc. else: # tuple, list, etc.
self._socket.set_proxy(*self._proxy) self._socket.set_proxy(*self._proxy)
def connect(self, ip, port): def connect(self, ip, port, timeout):
"""Connects to the specified IP and port number""" """Connects to the specified IP and port number.
'timeout' must be given in seconds
"""
if not self.connected: if not self.connected:
self._socket.settimeout(timeout)
self._socket.connect((ip, port)) self._socket.connect((ip, port))
self._socket.setblocking(False) self._socket.setblocking(False)
self.connected = True self.connected = True
@ -80,7 +83,7 @@ class TcpClient:
# Set the starting time so we can # Set the starting time so we can
# calculate whether the timeout should fire # calculate whether the timeout should fire
start_time = datetime.now() if timeout else None start_time = datetime.now() if timeout is not None else None
with BufferedWriter(BytesIO(), buffer_size=size) as buffer: with BufferedWriter(BytesIO(), buffer_size=size) as buffer:
bytes_left = size bytes_left = size
@ -93,8 +96,9 @@ class TcpClient:
try: try:
partial = self._socket.recv(bytes_left) partial = self._socket.recv(bytes_left)
if len(partial) == 0: if len(partial) == 0:
self.connected = False
raise ConnectionResetError( raise ConnectionResetError(
'The server has closed the connection (recv() returned 0 bytes).') 'The server has closed the connection.')
buffer.write(partial) buffer.write(partial)
bytes_left -= len(partial) bytes_left -= len(partial)
@ -104,7 +108,7 @@ class TcpClient:
time.sleep(self.delay) time.sleep(self.delay)
# Check if the timeout finished # Check if the timeout finished
if timeout: if timeout is not None:
time_passed = datetime.now() - start_time time_passed = datetime.now() - start_time
if time_passed > timeout: if time_passed > timeout:
raise TimeoutError( raise TimeoutError(

View File

@ -0,0 +1,94 @@
import socket
import time
from datetime import datetime, timedelta
from io import BytesIO, BufferedWriter
from threading import Event, Lock, Thread, Condition
from ..errors import ReadCancelledError
class ThreadedTcpClient:
"""The main difference with the TcpClient class is that this one
will spawn a secondary thread that will be constantly reading
from the network and putting everything on another buffer.
"""
def __init__(self, proxy=None):
self.connected = False
self._proxy = proxy
self._recreate_socket()
# Support for multi-threading advantages and safety
self.cancelled = Event() # Has the read operation been cancelled?
self.delay = 0.1 # Read delay when there was no data available
self._lock = Lock()
self._buffer = []
self._read_thread = Thread(target=self._reading_thread, daemon=True)
self._cv = Condition() # Condition Variable
def _recreate_socket(self):
if self._proxy is None:
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
else:
import socks
self._socket = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)
if type(self._proxy) is dict:
self._socket.set_proxy(**self._proxy)
else: # tuple, list, etc.
self._socket.set_proxy(*self._proxy)
def connect(self, ip, port, timeout):
"""Connects to the specified IP and port number.
'timeout' must be given in seconds
"""
if not self.connected:
self._socket.settimeout(timeout)
self._socket.connect((ip, port))
self._socket.setblocking(False)
self.connected = True
def close(self):
"""Closes the connection"""
if self.connected:
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
self.connected = False
self._recreate_socket()
def write(self, data):
"""Writes (sends) the specified bytes to the connected peer"""
self._socket.sendall(data)
def read(self, size, timeout=timedelta(seconds=5)):
"""Reads (receives) a whole block of 'size bytes
from the connected peer.
A timeout can be specified, which will cancel the operation if
no data has been read in the specified time. If data was read
and it's waiting for more, the timeout will NOT cancel the
operation. Set to None for no timeout
"""
with self._cv:
print('wait for...')
self._cv.wait_for(lambda: len(self._buffer) >= size, timeout=timeout.seconds)
print('got', size)
result, self._buffer = self._buffer[:size], self._buffer[size:]
return result
def _reading_thread(self):
while True:
partial = self._socket.recv(4096)
if len(partial) == 0:
self.connected = False
raise ConnectionResetError(
'The server has closed the connection.')
with self._cv:
print('extended', len(partial))
self._buffer.extend(partial)
self._cv.notify()
def cancel_read(self):
"""Cancels the read operation IF it hasn't yet
started, raising a ReadCancelledError"""
self.cancelled.set()

View File

@ -1,4 +1,3 @@
import random
import time import time
from ..extensions import BinaryReader, BinaryWriter from ..extensions import BinaryReader, BinaryWriter
@ -42,17 +41,12 @@ class MtProtoPlainSender:
return response return response
def _get_new_msg_id(self): def _get_new_msg_id(self):
"""Generates a new message ID based on the current time (in ms) since epoch""" """Generates a new message ID based on the current time since epoch"""
# See https://core.telegram.org/mtproto/description#message-identifier-msg-id # See core.telegram.org/mtproto/description#message-identifier-msg-id
ms_time = int(time.time() * 1000) now = time.time()
new_msg_id = (((ms_time // 1000) << 32) nanoseconds = int((now - int(now)) * 1e+9)
| # "must approximately equal unix time*2^32" # "message identifiers are divisible by 4"
((ms_time % 1000) << 22) new_msg_id = (int(now) << 32) | (nanoseconds << 2)
| # "approximate moment in time the message was created"
random.randint(0, 524288)
<< 2) # "message identifiers are divisible by 4"
# Ensure that we always return a message ID which is higher than the previous one
if self._last_msg_id >= new_msg_id: if self._last_msg_id >= new_msg_id:
new_msg_id = self._last_msg_id + 4 new_msg_id = self._last_msg_id + 4

View File

@ -17,7 +17,7 @@ class MtProtoSender:
"""MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)""" """MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)"""
def __init__(self, transport, session): def __init__(self, transport, session):
self._transport = transport self.transport = transport
self.session = session self.session = session
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
@ -33,11 +33,14 @@ class MtProtoSender:
def connect(self): def connect(self):
"""Connects to the server""" """Connects to the server"""
self._transport.connect() self.transport.connect()
def is_connected(self):
return self.transport.is_connected()
def disconnect(self): def disconnect(self):
"""Disconnects from the server""" """Disconnects from the server"""
self._transport.close() self.transport.close()
# region Send and receive # region Send and receive
@ -73,11 +76,12 @@ class MtProtoSender:
del self._need_confirmation[:] del self._need_confirmation[:]
def receive(self, request=None, timeout=timedelta(seconds=5), updates=None): def receive(self, request=None, updates=None, **kwargs):
"""Receives the specified MTProtoRequest ("fills in it" """Receives the specified MTProtoRequest ("fills in it"
the received data). This also restores the updates thread. the received data). This also restores the updates thread.
An optional timeout can be specified to cancel the operation
if no data has been read after its time delta. An optional named parameter 'timeout' can be specified if
one desires to override 'self.transport.timeout'.
If 'request' is None, a single item will be read into If 'request' is None, a single item will be read into
the 'updates' list (which cannot be None). the 'updates' list (which cannot be None).
@ -96,8 +100,8 @@ class MtProtoSender:
# or, if there is no request, until we read an update # or, if there is no request, until we read an update
while (request and not request.confirm_received) or \ while (request and not request.confirm_received) or \
(not request and not updates): (not request and not updates):
self._logger.info('Trying to .receive() the request result...') self._logger.debug('Trying to .receive() the request result...')
seq, body = self._transport.receive(timeout) seq, body = self.transport.receive(**kwargs)
message, remote_msg_id, remote_seq = self._decode_msg(body) message, remote_msg_id, remote_seq = self._decode_msg(body)
with BinaryReader(message) as reader: with BinaryReader(message) as reader:
@ -110,21 +114,19 @@ class MtProtoSender:
self._pending_receive.remove(request) self._pending_receive.remove(request)
except ValueError: pass except ValueError: pass
self._logger.info('Request result received') self._logger.debug('Request result received')
self._logger.debug('receive() released the lock') self._logger.debug('receive() released the lock')
def receive_updates(self, timeout=timedelta(seconds=5)): def receive_updates(self, **kwargs):
"""Receives one or more update objects """Wrapper for .receive(request=None, updates=[])"""
and returns them as a list
"""
updates = [] updates = []
self.receive(timeout=timeout, updates=updates) self.receive(updates=updates, **kwargs)
return updates return updates
def cancel_receive(self): def cancel_receive(self):
"""Cancels any pending receive operation """Cancels any pending receive operation
by raising a ReadCancelledError""" by raising a ReadCancelledError"""
self._transport.cancel_receive() self.transport.cancel_receive()
# endregion # endregion
@ -141,7 +143,7 @@ class MtProtoSender:
plain_writer.write_long(self.session.id, signed=False) plain_writer.write_long(self.session.id, signed=False)
plain_writer.write_long(request.request_msg_id) plain_writer.write_long(request.request_msg_id)
plain_writer.write_int( plain_writer.write_int(
self.session.generate_sequence(request.confirmed)) self.session.generate_sequence(request.content_related))
plain_writer.write_int(len(packet)) plain_writer.write_int(len(packet))
plain_writer.write(packet) plain_writer.write(packet)
@ -157,7 +159,7 @@ class MtProtoSender:
self.session.auth_key.key_id, signed=False) self.session.auth_key.key_id, signed=False)
cipher_writer.write(msg_key) cipher_writer.write(msg_key)
cipher_writer.write(cipher_text) cipher_writer.write(cipher_text)
self._transport.send(cipher_writer.get_bytes()) self.transport.send(cipher_writer.get_bytes())
def _decode_msg(self, body): def _decode_msg(self, body):
"""Decodes an received encrypted message body bytes""" """Decodes an received encrypted message body bytes"""
@ -224,10 +226,10 @@ class MtProtoSender:
ack = reader.tgread_object() ack = reader.tgread_object()
for r in self._pending_receive: for r in self._pending_receive:
if r.request_msg_id in ack.msg_ids: if r.request_msg_id in ack.msg_ids:
self._logger.warning('Ack found for the a request') self._logger.debug('Ack found for the a request')
if self.logging_out: if self.logging_out:
self._logger.info('Message ack confirmed a request') self._logger.debug('Message ack confirmed a request')
r.confirm_received = True r.confirm_received = True
return True return True
@ -245,7 +247,7 @@ class MtProtoSender:
return True return True
self._logger.warning('Unknown message: {}'.format(hex(code))) self._logger.debug('Unknown message: {}'.format(hex(code)))
return False return False
# endregion # endregion
@ -255,13 +257,13 @@ class MtProtoSender:
def _handle_pong(self, msg_id, sequence, reader): def _handle_pong(self, msg_id, sequence, reader):
self._logger.debug('Handling pong') self._logger.debug('Handling pong')
reader.read_int(signed=False) # code reader.read_int(signed=False) # code
received_msg_id = reader.read_long(signed=False) received_msg_id = reader.read_long()
try: try:
request = next(r for r in self._pending_receive request = next(r for r in self._pending_receive
if r.request_msg_id == received_msg_id) if r.request_msg_id == received_msg_id)
self._logger.warning('Pong confirmed a request') self._logger.debug('Pong confirmed a request')
request.confirm_received = True request.confirm_received = True
except StopIteration: pass except StopIteration: pass
@ -272,23 +274,28 @@ class MtProtoSender:
reader.read_int(signed=False) # code reader.read_int(signed=False) # code
size = reader.read_int() size = reader.read_int()
for _ in range(size): for _ in range(size):
inner_msg_id = reader.read_long(signed=False) inner_msg_id = reader.read_long()
reader.read_int() # inner_sequence reader.read_int() # inner_sequence
inner_length = reader.read_int() inner_length = reader.read_int()
begin_position = reader.tell_position() begin_position = reader.tell_position()
# Note that this code is IMPORTANT for skipping RPC results of # Note that this code is IMPORTANT for skipping RPC results of
# lost requests (i.e., ones from the previous connection session) # lost requests (i.e., ones from the previous connection session)
if not self._process_msg( try:
inner_msg_id, sequence, reader, updates): if not self._process_msg(
inner_msg_id, sequence, reader, updates):
reader.set_position(begin_position + inner_length)
except:
# If any error is raised, something went wrong; skip the packet
reader.set_position(begin_position + inner_length) reader.set_position(begin_position + inner_length)
raise
return True return True
def _handle_bad_server_salt(self, msg_id, sequence, reader): def _handle_bad_server_salt(self, msg_id, sequence, reader):
self._logger.debug('Handling bad server salt') self._logger.debug('Handling bad server salt')
reader.read_int(signed=False) # code reader.read_int(signed=False) # code
bad_msg_id = reader.read_long(signed=False) bad_msg_id = reader.read_long()
reader.read_int() # bad_msg_seq_no reader.read_int() # bad_msg_seq_no
reader.read_int() # error_code reader.read_int() # error_code
new_salt = reader.read_long(signed=False) new_salt = reader.read_long(signed=False)
@ -306,7 +313,7 @@ class MtProtoSender:
def _handle_bad_msg_notification(self, msg_id, sequence, reader): def _handle_bad_msg_notification(self, msg_id, sequence, reader):
self._logger.debug('Handling bad message notification') self._logger.debug('Handling bad message notification')
reader.read_int(signed=False) # code reader.read_int(signed=False) # code
reader.read_long(signed=False) # request_id reader.read_long() # request_id
reader.read_int() # request_sequence reader.read_int() # request_sequence
error_code = reader.read_int() error_code = reader.read_int()
@ -316,8 +323,8 @@ class MtProtoSender:
# Use the current msg_id to determine the right time offset. # Use the current msg_id to determine the right time offset.
self.session.update_time_offset(correct_msg_id=msg_id) self.session.update_time_offset(correct_msg_id=msg_id)
self.session.save() self.session.save()
self._logger.warning('Read Bad Message error: ' + str(error)) self._logger.debug('Read Bad Message error: ' + str(error))
self._logger.info('Attempting to use the correct time offset.') self._logger.debug('Attempting to use the correct time offset.')
return True return True
else: else:
raise error raise error
@ -325,7 +332,7 @@ class MtProtoSender:
def _handle_rpc_result(self, msg_id, sequence, reader): def _handle_rpc_result(self, msg_id, sequence, reader):
self._logger.debug('Handling RPC result') self._logger.debug('Handling RPC result')
reader.read_int(signed=False) # code reader.read_int(signed=False) # code
request_id = reader.read_long(signed=False) request_id = reader.read_long()
inner_code = reader.read_int(signed=False) inner_code = reader.read_int(signed=False)
try: try:
@ -344,7 +351,7 @@ class MtProtoSender:
self._need_confirmation.append(request_id) self._need_confirmation.append(request_id)
self._send_acknowledges() self._send_acknowledges()
self._logger.warning('Read RPC error: %s', str(error)) self._logger.debug('Read RPC error: %s', str(error))
if isinstance(error, InvalidDCError): if isinstance(error, InvalidDCError):
# Must resend this request, if any # Must resend this request, if any
if request: if request:
@ -366,7 +373,7 @@ class MtProtoSender:
else: else:
# If it's really a result for RPC from previous connection # If it's really a result for RPC from previous connection
# session, it will be skipped by the handle_container() # session, it will be skipped by the handle_container()
self._logger.warning('Lost request will be skipped.') self._logger.debug('Lost request will be skipped.')
return False return False
def _handle_gzip_packed(self, msg_id, sequence, reader, updates): def _handle_gzip_packed(self, msg_id, sequence, reader, updates):

View File

@ -1,4 +1,4 @@
from binascii import crc32 from zlib import crc32
from datetime import timedelta from datetime import timedelta
from ..errors import InvalidChecksumError from ..errors import InvalidChecksumError
@ -7,19 +7,26 @@ from ..extensions import BinaryWriter
class TcpTransport: class TcpTransport:
def __init__(self, ip_address, port, proxy=None): def __init__(self, ip_address, port,
proxy=None, timeout=timedelta(seconds=5)):
self.ip = ip_address self.ip = ip_address
self.port = port self.port = port
self.tcp_client = TcpClient(proxy) self.tcp_client = TcpClient(proxy)
self.timeout = timeout
self.send_counter = 0 self.send_counter = 0
def connect(self): def connect(self):
"""Connects to the specified IP address and port""" """Connects to the specified IP address and port"""
self.send_counter = 0 self.send_counter = 0
self.tcp_client.connect(self.ip, self.port) self.tcp_client.connect(self.ip, self.port,
timeout=round(self.timeout.seconds))
def is_connected(self):
return self.tcp_client.connected
# Original reference: https://core.telegram.org/mtproto#tcp-transport # Original reference: https://core.telegram.org/mtproto#tcp-transport
# The packets are encoded as: total length, sequence number, packet and checksum (CRC32) # The packets are encoded as:
# total length, sequence number, packet and checksum (CRC32)
def send(self, packet): def send(self, packet):
"""Sends the given packet (bytes array) to the connected peer""" """Sends the given packet (bytes array) to the connected peer"""
if not self.tcp_client.connected: if not self.tcp_client.connected:
@ -36,10 +43,14 @@ class TcpTransport:
self.send_counter += 1 self.send_counter += 1
self.tcp_client.write(writer.get_bytes()) self.tcp_client.write(writer.get_bytes())
def receive(self, timeout=timedelta(seconds=5)): def receive(self, **kwargs):
"""Receives a TCP message (tuple(sequence number, body)) from the connected peer. """Receives a TCP message (tuple(sequence number, body)) from the
There is a default timeout of 5 seconds before the operation is cancelled. connected peer.
Timeout can be set to None for no timeout"""
If a named 'timeout' parameter is present, it will override
'self.timeout', and this can be a 'timedelta' or 'None'.
"""
timeout = kwargs.get('timeout', self.timeout)
# First read everything we need # First read everything we need
packet_length_bytes = self.tcp_client.read(4, timeout) packet_length_bytes = self.tcp_client.read(4, timeout)

View File

@ -5,22 +5,27 @@ from os import path
# Import some externalized utilities to work with the Telegram types and more # Import some externalized utilities to work with the Telegram types and more
from . import helpers as utils from . import helpers as utils
from .errors import RPCError, FloodWaitError from .errors import (
RPCError, FloodWaitError, FileMigrateError, TypeNotFoundError
)
from .network import authenticator, MtProtoSender, TcpTransport from .network import authenticator, MtProtoSender, TcpTransport
from .utils import get_appropriated_part_size from .utils import get_appropriated_part_size
# For sending and receiving requests # For sending and receiving requests
from .tl import MTProtoRequest from .tl import TLObject, JsonSession
from .tl.all_tlobjects import layer from .tl.all_tlobjects import layer
from .tl.functions import (InitConnectionRequest, InvokeWithLayerRequest) from .tl.functions import (InitConnectionRequest, InvokeWithLayerRequest)
# Initial request # Initial request
from .tl.functions.help import GetConfigRequest from .tl.functions.help import GetConfigRequest
from .tl.functions.auth import ImportAuthorizationRequest from .tl.functions.auth import (
ImportAuthorizationRequest, ExportAuthorizationRequest
)
# Easier access for working with media # Easier access for working with media
from .tl.functions.upload import ( from .tl.functions.upload import (
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest) GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest
)
# All the types we need to work with # All the types we need to work with
from .tl.types import InputFile, InputFileBig from .tl.types import InputFile, InputFileBig
@ -47,11 +52,12 @@ class TelegramBareClient:
""" """
# Current TelegramClient version # Current TelegramClient version
__version__ = '0.11' __version__ = '0.11.5'
# region Initialization # region Initialization
def __init__(self, session, api_id, api_hash, proxy=None): def __init__(self, session, api_id, api_hash,
proxy=None, timeout=timedelta(seconds=5)):
"""Initializes the Telegram client with the specified API ID and Hash. """Initializes the Telegram client with the specified API ID and Hash.
Session must always be a Session instance, and an optional proxy Session must always be a Session instance, and an optional proxy
can also be specified to be used on the connection. can also be specified to be used on the connection.
@ -60,11 +66,17 @@ class TelegramBareClient:
self.api_id = int(api_id) self.api_id = int(api_id)
self.api_hash = api_hash self.api_hash = api_hash
self.proxy = proxy self.proxy = proxy
self._timeout = timeout
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
# Cache "exported" senders 'dc_id: TelegramBareClient' and
# their corresponding sessions not to recreate them all
# the time since it's a (somewhat expensive) process.
self._cached_clients = {}
# These will be set later # These will be set later
self.dc_options = None self.dc_options = None
self.sender = None self._sender = None
# endregion # endregion
@ -79,8 +91,16 @@ class TelegramBareClient:
If 'exported_auth' is not None, it will be used instead to If 'exported_auth' is not None, it will be used instead to
determine the authorization key for the current session. determine the authorization key for the current session.
""" """
if self._sender and self._sender.is_connected():
self._logger.debug(
'Attempted to connect when the client was already connected.'
)
return
transport = TcpTransport(self.session.server_address, transport = TcpTransport(self.session.server_address,
self.session.port, proxy=self.proxy) self.session.port,
proxy=self.proxy,
timeout=self._timeout)
try: try:
if not self.session.auth_key: if not self.session.auth_key:
@ -89,8 +109,8 @@ class TelegramBareClient:
self.session.save() self.session.save()
self.sender = MtProtoSender(transport, self.session) self._sender = MtProtoSender(transport, self.session)
self.sender.connect() self._sender.connect()
# Now it's time to send an InitConnectionRequest # Now it's time to send an InitConnectionRequest
# This must always be invoked with the layer we'll be using # This must always be invoked with the layer we'll be using
@ -106,34 +126,40 @@ class TelegramBareClient:
system_version=self.session.system_version, system_version=self.session.system_version,
app_version=self.session.app_version, app_version=self.session.app_version,
lang_code=self.session.lang_code, lang_code=self.session.lang_code,
system_lang_code=self.session.system_lang_code,
lang_pack='', # "langPacks are for official apps only"
query=query) query=query)
result = self.invoke( result = self(InvokeWithLayerRequest(
InvokeWithLayerRequest( layer=layer, query=request
layer=layer, query=request)) ))
if exported_auth is not None: if exported_auth is not None:
# TODO Don't actually need this for exported authorizations, result = self(GetConfigRequest())
# they're only valid on such data center.
result = self.invoke(GetConfigRequest())
# We're only interested in the DC options, # We're only interested in the DC options,
# although many other options are available! # although many other options are available!
self.dc_options = result.dc_options self.dc_options = result.dc_options
return True return True
except TypeNotFoundError as e:
# This is fine, probably layer migration
self._logger.debug('Found invalid item, probably migrating', e)
self.disconnect()
self.connect(exported_auth=exported_auth)
except (RPCError, ConnectionError) as error: except (RPCError, ConnectionError) as error:
# Probably errors from the previous session, ignore them # Probably errors from the previous session, ignore them
self.disconnect() self.disconnect()
self._logger.warning('Could not stabilise initial connection: {}' self._logger.debug('Could not stabilise initial connection: {}'
.format(error)) .format(error))
return False return False
def disconnect(self): def disconnect(self):
"""Disconnects from the Telegram server""" """Disconnects from the Telegram server"""
if self.sender: if self._sender:
self.sender.disconnect() self._sender.disconnect()
self.sender = None self._sender = None
def reconnect(self, new_dc=None): def reconnect(self, new_dc=None):
"""Disconnects and connects again (effectively reconnecting). """Disconnects and connects again (effectively reconnecting).
@ -154,6 +180,30 @@ class TelegramBareClient:
# endregion # endregion
# region Properties
def set_timeout(self, timeout):
if timeout is None:
self._timeout = None
elif isinstance(timeout, int) or isinstance(timeout, float):
self._timeout = timedelta(seconds=timeout)
elif isinstance(timeout, timedelta):
self._timeout = timeout
else:
raise ValueError(
'{} is not a valid type for a timeout'.format(type(timeout))
)
if self._sender:
self._sender.transport.timeout = self._timeout
def get_timeout(self):
return self._timeout
timeout = property(get_timeout, set_timeout)
# endregion
# region Working with different Data Centers # region Working with different Data Centers
def _get_dc(self, dc_id): def _get_dc(self, dc_id):
@ -165,40 +215,88 @@ class TelegramBareClient:
return next(dc for dc in self.dc_options if dc.id == dc_id) return next(dc for dc in self.dc_options if dc.id == dc_id)
def _get_exported_client(self, dc_id,
init_connection=False,
bypass_cache=False):
"""Gets a cached exported TelegramBareClient for the desired DC.
If it's the first time retrieving the TelegramBareClient, the
current authorization is exported to the new DC so that
it can be used there, and the connection is initialized.
If after using the sender a ConnectionResetError is raised,
this method should be called again with init_connection=True
in order to perform the reconnection.
If bypass_cache is True, a new client will be exported and
it will not be cached.
"""
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
# for clearly showing how to export the authorization! ^^
client = self._cached_clients.get(dc_id)
if client and not bypass_cache:
if init_connection:
client.reconnect()
return client
else:
dc = self._get_dc(dc_id)
# Export the current authorization to the new DC.
export_auth = self(ExportAuthorizationRequest(dc_id))
# Create a temporary session for this IP address, which needs
# to be different because each auth_key is unique per DC.
#
# Construct this session with the connection parameters
# (system version, device model...) from the current one.
session = JsonSession(self.session)
session.server_address = dc.ip_address
session.port = dc.port
client = TelegramBareClient(
session, self.api_id, self.api_hash,
timeout=self._timeout
)
client.connect(exported_auth=export_auth)
if not bypass_cache:
# Don't go through this expensive process every time.
self._cached_clients[dc_id] = client
return client
# endregion # endregion
# region Invoking Telegram requests # region Invoking Telegram requests
def invoke(self, request, timeout=timedelta(seconds=5), updates=None): def invoke(self, request, updates=None):
"""Invokes (sends) a MTProtoRequest and returns (receives) its result. """Invokes (sends) a MTProtoRequest and returns (receives) its result.
An optional timeout can be specified to cancel the operation if no
result is received within such time, or None to disable any timeout.
If 'updates' is not None, all read update object will be put If 'updates' is not None, all read update object will be put
in such list. Otherwise, update objects will be ignored. in such list. Otherwise, update objects will be ignored.
""" """
if not isinstance(request, MTProtoRequest): if not isinstance(request, TLObject) and not request.content_related:
raise ValueError('You can only invoke MtProtoRequests') raise ValueError('You can only invoke requests, not types!')
if not self.sender: if not self._sender:
raise ValueError('You must be connected to invoke requests!') raise ValueError('You must be connected to invoke requests!')
try: try:
self.sender.send(request) self._sender.send(request)
self.sender.receive(request, timeout, updates=updates) self._sender.receive(request, updates=updates)
return request.result return request.result
except ConnectionResetError: except ConnectionResetError:
self._logger.info('Server disconnected us. Reconnecting and ' self._logger.debug('Server disconnected us. Reconnecting and '
'resending request...') 'resending request...')
self.reconnect() self.reconnect()
return self.invoke(request, timeout=timeout) return self.invoke(request)
except FloodWaitError: except FloodWaitError:
self.disconnect() self.disconnect()
raise raise
# Let people use client(SomeRequest()) instead client.invoke(...)
__call__ = invoke
# endregion # endregion
# region Uploading media # region Uploading media
@ -250,7 +348,7 @@ class TelegramBareClient:
else: else:
request = SaveFilePartRequest(file_id, part_index, part) request = SaveFilePartRequest(file_id, part_index, part)
result = self.invoke(request) result = self(request)
if result: if result:
if not is_large: if not is_large:
# No need to update the hash if it's a large file # No need to update the hash if it's a large file
@ -305,12 +403,21 @@ class TelegramBareClient:
else: else:
f = file f = file
# The used client will change if FileMigrateError occurs
client = self
try: try:
offset_index = 0 offset_index = 0
while True: while True:
offset = offset_index * part_size offset = offset_index * part_size
result = self.invoke(
GetFileRequest(input_location, offset, part_size)) try:
result = client(
GetFileRequest(input_location, offset, part_size))
except FileMigrateError as e:
client = self._get_exported_client(e.new_dc)
continue
offset_index += 1 offset_index += 1
# If we have received no data (0 bytes), the file is over # If we have received no data (0 bytes), the file is over

View File

@ -1,20 +1,20 @@
from datetime import timedelta from datetime import timedelta
from mimetypes import guess_type from mimetypes import guess_type
from threading import Event, RLock, Thread from threading import Event, RLock, Thread
from time import sleep from time import sleep, time
from . import TelegramBareClient from . import TelegramBareClient
# Import some externalized utilities to work with the Telegram types and more # Import some externalized utilities to work with the Telegram types and more
from . import helpers as utils from . import helpers as utils
from .errors import (RPCError, UnauthorizedError, InvalidParameterError, from .errors import (RPCError, UnauthorizedError, InvalidParameterError,
ReadCancelledError, FileMigrateError, PhoneMigrateError, ReadCancelledError, PhoneCodeEmptyError,
NetworkMigrateError, UserMigrateError, PhoneCodeEmptyError, PhoneMigrateError, NetworkMigrateError, UserMigrateError,
PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError,
PhoneCodeInvalidError, InvalidChecksumError) PhoneCodeInvalidError, InvalidChecksumError)
# For sending and receiving requests # For sending and receiving requests
from .tl import MTProtoRequest, Session, JsonSession from .tl import Session, JsonSession
# Required to get the password salt # Required to get the password salt
from .tl.functions.account import GetPasswordRequest from .tl.functions.account import GetPasswordRequest
@ -24,9 +24,6 @@ from .tl.functions.auth import (CheckPasswordRequest, LogOutRequest,
SendCodeRequest, SignInRequest, SendCodeRequest, SignInRequest,
SignUpRequest, ImportBotAuthorizationRequest) SignUpRequest, ImportBotAuthorizationRequest)
# Required to work with different data centers
from .tl.functions.auth import ExportAuthorizationRequest
# Easier access to common methods # Easier access to common methods
from .tl.functions.messages import ( from .tl.functions.messages import (
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest, GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
@ -35,6 +32,9 @@ from .tl.functions.messages import (
# For .get_me() and ensuring we're authorized # For .get_me() and ensuring we're authorized
from .tl.functions.users import GetUsersRequest from .tl.functions.users import GetUsersRequest
# So the server doesn't stop sending updates to us
from .tl.functions import PingRequest
# All the types we need to work with # All the types we need to work with
from .tl.types import ( from .tl.types import (
ChatPhotoEmpty, DocumentAttributeAudio, DocumentAttributeFilename, ChatPhotoEmpty, DocumentAttributeAudio, DocumentAttributeFilename,
@ -61,7 +61,9 @@ class TelegramClient(TelegramBareClient):
def __init__(self, session, api_id, api_hash, proxy=None, def __init__(self, session, api_id, api_hash, proxy=None,
device_model=None, system_version=None, device_model=None, system_version=None,
app_version=None, lang_code=None): app_version=None, lang_code=None,
system_lang_code=None,
timeout=timedelta(seconds=5)):
"""Initializes the Telegram client with the specified API ID and Hash. """Initializes the Telegram client with the specified API ID and Hash.
Session can either be a `str` object (filename for the .session) Session can either be a `str` object (filename for the .session)
@ -70,12 +72,12 @@ class TelegramClient(TelegramBareClient):
session - remember to '.log_out()'! session - remember to '.log_out()'!
Default values for the optional parameters if left as None are: Default values for the optional parameters if left as None are:
device_model = platform.node() device_model = platform.node()
system_version = platform.system() system_version = platform.system()
app_version = TelegramClient.__version__ app_version = TelegramClient.__version__
lang_code = 'en' lang_code = 'en'
system_lang_code = lang_code
""" """
if not api_id or not api_hash: if not api_id or not api_hash:
raise PermissionError( raise PermissionError(
"Your API ID or Hash cannot be empty or None. " "Your API ID or Hash cannot be empty or None. "
@ -85,20 +87,23 @@ class TelegramClient(TelegramBareClient):
# TODO JsonSession until migration is complete (by v1.0) # TODO JsonSession until migration is complete (by v1.0)
if isinstance(session, str) or session is None: if isinstance(session, str) or session is None:
session = JsonSession.try_load_or_create_new(session) session = JsonSession.try_load_or_create_new(session)
elif not isinstance(session, Session): elif not isinstance(session, Session) and not isinstance(session, JsonSession):
raise ValueError( raise ValueError(
'The given session must be a str or a Session instance.') 'The given session must be a str or a Session instance.')
super().__init__(session, api_id, api_hash, proxy) super().__init__(session, api_id, api_hash, proxy, timeout=timeout)
# Safety across multiple threads (for the updates thread) # Safety across multiple threads (for the updates thread)
self._lock = RLock() self._lock = RLock()
# Methods to be called when an update is received # Updates-related members
self._update_handlers = [] self._update_handlers = []
self._updates_thread_running = Event() self._updates_thread_running = Event()
self._updates_thread_receiving = Event() self._updates_thread_receiving = Event()
self._next_ping_at = 0
self.ping_interval = 60 # Seconds
# Used on connection - the user may modify these and reconnect # Used on connection - the user may modify these and reconnect
if device_model: if device_model:
self.session.device_model = device_model self.session.device_model = device_model
@ -112,10 +117,9 @@ class TelegramClient(TelegramBareClient):
if lang_code: if lang_code:
self.session.lang_code = lang_code self.session.lang_code = lang_code
# Cache "exported" senders 'dc_id: MtProtoSender' and self.session.system_lang_code = \
# their corresponding sessions not to recreate them all system_lang_code if system_lang_code else self.session.lang_code
# the time since it's a (somewhat expensive) process.
self._cached_clients = {}
self._updates_thread = None self._updates_thread = None
self._phone_code_hashes = {} self._phone_code_hashes = {}
@ -129,15 +133,17 @@ class TelegramClient(TelegramBareClient):
not the same as authenticating the desired user itself, which not the same as authenticating the desired user itself, which
may require a call (or several) to 'sign_in' for the first time. may require a call (or several) to 'sign_in' for the first time.
The specified timeout will be used on internal .invoke()'s.
*args will be ignored. *args will be ignored.
""" """
return super(TelegramClient, self).connect() return super().connect()
def disconnect(self): def disconnect(self):
"""Disconnects from the Telegram server """Disconnects from the Telegram server
and stops all the spawned threads""" and stops all the spawned threads"""
self._set_updates_thread(running=False) self._set_updates_thread(running=False)
super(TelegramClient, self).disconnect() super().disconnect()
# Also disconnect all the cached senders # Also disconnect all the cached senders
for sender in self._cached_clients.values(): for sender in self._cached_clients.values():
@ -149,52 +155,6 @@ class TelegramClient(TelegramBareClient):
# region Working with different connections # region Working with different connections
def _get_exported_client(self, dc_id,
init_connection=False,
bypass_cache=False):
"""Gets a cached exported TelegramBareClient for the desired DC.
If it's the first time retrieving the TelegramBareClient, the
current authorization is exported to the new DC so that
it can be used there, and the connection is initialized.
If after using the sender a ConnectionResetError is raised,
this method should be called again with init_connection=True
in order to perform the reconnection.
If bypass_cache is True, a new client will be exported and
it will not be cached.
"""
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
# for clearly showing how to export the authorization! ^^
client = self._cached_clients.get(dc_id)
if client and not bypass_cache:
if init_connection:
client.reconnect()
return client
else:
dc = self._get_dc(dc_id)
# Export the current authorization to the new DC.
export_auth = self.invoke(ExportAuthorizationRequest(dc_id))
# Create a temporary session for this IP address, which needs
# to be different because each auth_key is unique per DC.
#
# Construct this session with the connection parameters
# (system version, device model...) from the current one.
session = JsonSession(self.session)
session.server_address = dc.ip_address
session.port = dc.port
client = TelegramBareClient(session, self.api_id, self.api_hash)
client.connect(exported_auth=export_auth)
if not bypass_cache:
# Don't go through this expensive process every time.
self._cached_clients[dc_id] = client
return client
def create_new_connection(self, on_dc=None): def create_new_connection(self, on_dc=None):
"""Creates a new connection which can be used in parallel """Creates a new connection which can be used in parallel
with the original TelegramClient. A TelegramBareClient with the original TelegramClient. A TelegramBareClient
@ -206,15 +166,10 @@ class TelegramClient(TelegramBareClient):
If the client is meant to be used on a different data If the client is meant to be used on a different data
center, the data center ID should be specified instead. center, the data center ID should be specified instead.
Note that TelegramBareClients will not handle automatic
reconnection (i.e. switching to another data center to
download media), and InvalidDCError will be raised in
such case.
""" """
if on_dc is None: if on_dc is None:
client = TelegramBareClient(self.session, self.api_id, self.api_hash, client = TelegramBareClient(
proxy=self.proxy) self.session, self.api_id, self.api_hash, proxy=self.proxy)
client.connect() client.connect()
else: else:
client = self._get_exported_client(on_dc, bypass_cache=True) client = self._get_exported_client(on_dc, bypass_cache=True)
@ -225,7 +180,7 @@ class TelegramClient(TelegramBareClient):
# region Telegram requests functions # region Telegram requests functions
def invoke(self, request, timeout=timedelta(seconds=5), *args): def invoke(self, request, *args):
"""Invokes (sends) a MTProtoRequest and returns (receives) its result. """Invokes (sends) a MTProtoRequest and returns (receives) its result.
An optional timeout can be specified to cancel the operation if no An optional timeout can be specified to cancel the operation if no
@ -233,21 +188,16 @@ class TelegramClient(TelegramBareClient):
*args will be ignored. *args will be ignored.
""" """
if not issubclass(type(request), MTProtoRequest):
raise ValueError('You can only invoke MtProtoRequests')
if not self.sender:
raise ValueError('You must be connected to invoke requests!')
if self._updates_thread_receiving.is_set(): if self._updates_thread_receiving.is_set():
self.sender.cancel_receive() self._sender.cancel_receive()
try: try:
self._lock.acquire() self._lock.acquire()
updates = [] if self._update_handlers else None updates = [] if self._update_handlers else None
result = super(TelegramClient, self).invoke( result = super().invoke(
request, timeout=timeout, updates=updates) request, updates=updates
)
if updates: if updates:
for update in updates: for update in updates:
@ -258,18 +208,20 @@ class TelegramClient(TelegramBareClient):
return result return result
except (PhoneMigrateError, NetworkMigrateError, UserMigrateError) as e: except (PhoneMigrateError, NetworkMigrateError, UserMigrateError) as e:
self._logger.info('DC error when invoking request, ' self._logger.debug('DC error when invoking request, '
'attempting to reconnect at DC {}' 'attempting to reconnect at DC {}'
.format(e.new_dc)) .format(e.new_dc))
self.reconnect(new_dc=e.new_dc) self.reconnect(new_dc=e.new_dc)
return self.invoke(request, timeout=timeout) return self.invoke(request)
finally: finally:
self._lock.release() self._lock.release()
def invoke_on_dc(self, request, dc_id, # Let people use client(SomeRequest()) instead client.invoke(...)
timeout=timedelta(seconds=5), reconnect=False): __call__ = invoke
def invoke_on_dc(self, request, dc_id, reconnect=False):
"""Invokes the given request on a different DC """Invokes the given request on a different DC
by making use of the exported MtProtoSenders. by making use of the exported MtProtoSenders.
@ -286,8 +238,7 @@ class TelegramClient(TelegramBareClient):
if reconnect: if reconnect:
raise raise
else: else:
return self.invoke_on_dc(request, dc_id, return self.invoke_on_dc(request, dc_id, reconnect=True)
timeout=timeout, reconnect=True)
# region Authorization requests # region Authorization requests
@ -298,7 +249,7 @@ class TelegramClient(TelegramBareClient):
def send_code_request(self, phone_number): def send_code_request(self, phone_number):
"""Sends a code request to the specified phone number""" """Sends a code request to the specified phone number"""
result = self.invoke( result = self(
SendCodeRequest(phone_number, self.api_id, self.api_hash)) SendCodeRequest(phone_number, self.api_id, self.api_hash))
self._phone_code_hashes[phone_number] = result.phone_code_hash self._phone_code_hashes[phone_number] = result.phone_code_hash
@ -324,7 +275,7 @@ class TelegramClient(TelegramBareClient):
'Please make sure to call send_code_request first.') 'Please make sure to call send_code_request first.')
try: try:
result = self.invoke(SignInRequest( result = self(SignInRequest(
phone_number, self._phone_code_hashes[phone_number], code)) phone_number, self._phone_code_hashes[phone_number], code))
except (PhoneCodeEmptyError, PhoneCodeExpiredError, except (PhoneCodeEmptyError, PhoneCodeExpiredError,
@ -332,12 +283,12 @@ class TelegramClient(TelegramBareClient):
return None return None
elif password: elif password:
salt = self.invoke(GetPasswordRequest()).current_salt salt = self(GetPasswordRequest()).current_salt
result = self.invoke( result = self(
CheckPasswordRequest(utils.get_password_hash(password, salt))) CheckPasswordRequest(utils.get_password_hash(password, salt)))
elif bot_token: elif bot_token:
result = self.invoke(ImportBotAuthorizationRequest( result = self(ImportBotAuthorizationRequest(
flags=0, bot_auth_token=bot_token, flags=0, bot_auth_token=bot_token,
api_id=self.api_id, api_hash=self.api_hash)) api_id=self.api_id, api_hash=self.api_hash))
@ -350,7 +301,7 @@ class TelegramClient(TelegramBareClient):
def sign_up(self, phone_number, code, first_name, last_name=''): def sign_up(self, phone_number, code, first_name, last_name=''):
"""Signs up to Telegram. Make sure you sent a code request first!""" """Signs up to Telegram. Make sure you sent a code request first!"""
result = self.invoke( result = self(
SignUpRequest( SignUpRequest(
phone_number=phone_number, phone_number=phone_number,
phone_code_hash=self._phone_code_hashes[phone_number], phone_code_hash=self._phone_code_hashes[phone_number],
@ -366,9 +317,9 @@ class TelegramClient(TelegramBareClient):
Returns True if everything went okay.""" Returns True if everything went okay."""
# Special flag when logging out (so the ack request confirms it) # Special flag when logging out (so the ack request confirms it)
self.sender.logging_out = True self._sender.logging_out = True
try: try:
self.invoke(LogOutRequest()) self(LogOutRequest())
self.disconnect() self.disconnect()
if not self.session.delete(): if not self.session.delete():
return False return False
@ -377,14 +328,14 @@ class TelegramClient(TelegramBareClient):
return True return True
except (RPCError, ConnectionError): except (RPCError, ConnectionError):
# Something happened when logging out, restore the state back # Something happened when logging out, restore the state back
self.sender.logging_out = False self._sender.logging_out = False
return False return False
def get_me(self): def get_me(self):
"""Gets "me" (the self user) which is currently authenticated, """Gets "me" (the self user) which is currently authenticated,
or None if the request fails (hence, not authenticated).""" or None if the request fails (hence, not authenticated)."""
try: try:
return self.invoke(GetUsersRequest([InputUserSelf()]))[0] return self(GetUsersRequest([InputUserSelf()]))[0]
except UnauthorizedError: except UnauthorizedError:
return None return None
@ -405,7 +356,7 @@ class TelegramClient(TelegramBareClient):
corresponding to that dialog. corresponding to that dialog.
""" """
r = self.invoke( r = self(
GetDialogsRequest( GetDialogsRequest(
offset_date=offset_date, offset_date=offset_date,
offset_id=offset_id, offset_id=offset_id,
@ -422,16 +373,18 @@ class TelegramClient(TelegramBareClient):
def send_message(self, def send_message(self,
entity, entity,
message, message,
no_web_page=False): link_preview=True):
"""Sends a message to the given entity (or input peer) """Sends a message to the given entity (or input peer)
and returns the sent message ID""" and returns the sent message ID"""
request = SendMessageRequest( request = SendMessageRequest(
peer=get_input_peer(entity), peer=get_input_peer(entity),
message=message, message=message,
entities=[], entities=[],
no_webpage=no_web_page no_webpage=not link_preview
) )
self.invoke(request) result = self(request)
for handler in self._update_handlers:
handler(result)
return request.random_id return request.random_id
def get_message_history(self, def get_message_history(self,
@ -456,15 +409,15 @@ class TelegramClient(TelegramBareClient):
:return: A tuple containing total message count and two more lists ([messages], [senders]). :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! Note that the sender can be null if it was not found!
""" """
result = self.invoke( result = self(GetHistoryRequest(
GetHistoryRequest( get_input_peer(entity),
get_input_peer(entity), limit=limit,
limit=limit, offset_date=offset_date,
offset_date=offset_date, offset_id=offset_id,
offset_id=offset_id, max_id=max_id,
max_id=max_id, min_id=min_id,
min_id=min_id, add_offset=add_offset
add_offset=add_offset)) ))
# The result may be a messages slice (not all messages were retrieved) # The result may be a messages slice (not all messages were retrieved)
# or simply a messages TLObject. In the later case, no "count" # or simply a messages TLObject. In the later case, no "count"
@ -498,7 +451,10 @@ class TelegramClient(TelegramBareClient):
else: else:
max_id = messages.id max_id = messages.id
return self.invoke(ReadHistoryRequest(peer=get_input_peer(entity), max_id=max_id)) return self(ReadHistoryRequest(
peer=get_input_peer(entity),
max_id=max_id
))
# endregion # endregion
@ -537,7 +493,7 @@ class TelegramClient(TelegramBareClient):
def send_media_file(self, input_media, entity): def send_media_file(self, input_media, entity):
"""Sends any input_media (contact, document, photo...) to the given entity""" """Sends any input_media (contact, document, photo...) to the given entity"""
self.invoke(SendMediaRequest( self(SendMediaRequest(
peer=get_input_peer(entity), peer=get_input_peer(entity),
media=input_media media=input_media
)) ))
@ -580,36 +536,41 @@ class TelegramClient(TelegramBareClient):
def download_msg_media(self, def download_msg_media(self,
message_media, message_media,
file_path, file,
add_extension=True, add_extension=True,
progress_callback=None): progress_callback=None):
"""Downloads the given MessageMedia (Photo, Document or Contact) """Downloads the given MessageMedia (Photo, Document or Contact)
into the desired file_path, optionally finding its extension automatically into the desired file (a stream or str), optionally finding its
The progress_callback should be a callback function which takes two parameters, extension automatically.
uploaded size (in bytes) and total file size (in bytes).
This will be called every time a part is downloaded""" The progress_callback should be a callback function which takes
two parameters, uploaded size and total file size (both in bytes).
This will be called every time a part is downloaded
"""
if type(message_media) == MessageMediaPhoto: if type(message_media) == MessageMediaPhoto:
return self.download_photo(message_media, file_path, add_extension, return self.download_photo(message_media, file, add_extension,
progress_callback) progress_callback)
elif type(message_media) == MessageMediaDocument: elif type(message_media) == MessageMediaDocument:
return self.download_document(message_media, file_path, return self.download_document(message_media, file,
add_extension, progress_callback) add_extension, progress_callback)
elif type(message_media) == MessageMediaContact: elif type(message_media) == MessageMediaContact:
return self.download_contact(message_media, file_path, return self.download_contact(message_media, file,
add_extension) add_extension)
def download_photo(self, def download_photo(self,
message_media_photo, message_media_photo,
file_path, file,
add_extension=False, add_extension=False,
progress_callback=None): progress_callback=None):
"""Downloads MessageMediaPhoto's largest size into the desired """Downloads MessageMediaPhoto's largest size into the desired file
file_path, optionally finding its extension automatically (a stream or str), optionally finding its extension automatically.
The progress_callback should be a callback function which takes two parameters,
uploaded size (in bytes) and total file size (in bytes). The progress_callback should be a callback function which takes
This will be called every time a part is downloaded""" two parameters, uploaded size and total file size (both in bytes).
This will be called every time a part is downloaded
"""
# Determine the photo and its largest size # Determine the photo and its largest size
photo = message_media_photo.photo photo = message_media_photo.photo
@ -617,8 +578,8 @@ class TelegramClient(TelegramBareClient):
file_size = largest_size.size file_size = largest_size.size
largest_size = largest_size.location largest_size = largest_size.location
if add_extension: if isinstance(file, str) and add_extension:
file_path += get_extension(message_media_photo) file += get_extension(message_media_photo)
# Download the media with the largest size input file location # Download the media with the largest size input file location
self.download_file( self.download_file(
@ -627,42 +588,45 @@ class TelegramClient(TelegramBareClient):
local_id=largest_size.local_id, local_id=largest_size.local_id,
secret=largest_size.secret secret=largest_size.secret
), ),
file_path, file,
file_size=file_size, file_size=file_size,
progress_callback=progress_callback progress_callback=progress_callback
) )
return file_path return file
def download_document(self, def download_document(self,
message_media_document, message_media_document,
file_path=None, file=None,
add_extension=True, add_extension=True,
progress_callback=None): progress_callback=None):
"""Downloads the given MessageMediaDocument into the desired """Downloads the given MessageMediaDocument into the desired file
file_path, optionally finding its extension automatically. (a stream or str), optionally finding its extension automatically.
If no file_path is given, it will try to be guessed from the document
The progress_callback should be a callback function which takes two parameters, If no file_path is given it will try to be guessed from the document.
uploaded size (in bytes) and total file size (in bytes).
This will be called every time a part is downloaded""" The progress_callback should be a callback function which takes
two parameters, uploaded size and total file size (both in bytes).
This will be called every time a part is downloaded
"""
document = message_media_document.document document = message_media_document.document
file_size = document.size file_size = document.size
# If no file path was given, try to guess it from the attributes # If no file path was given, try to guess it from the attributes
if file_path is None: if file is None:
for attr in document.attributes: for attr in document.attributes:
if type(attr) == DocumentAttributeFilename: if type(attr) == DocumentAttributeFilename:
file_path = attr.file_name file = attr.file_name
break # This attribute has higher preference break # This attribute has higher preference
elif type(attr) == DocumentAttributeAudio: elif type(attr) == DocumentAttributeAudio:
file_path = '{} - {}'.format(attr.performer, attr.title) file = '{} - {}'.format(attr.performer, attr.title)
if file_path is None: if file is None:
raise ValueError('Could not infer a file_path for the document' raise ValueError('Could not infer a file_path for the document'
'. Please provide a valid file_path manually') '. Please provide a valid file_path manually')
if add_extension: if isinstance(file, str) and add_extension:
file_path += get_extension(message_media_document) file += get_extension(message_media_document)
self.download_file( self.download_file(
InputDocumentFileLocation( InputDocumentFileLocation(
@ -670,74 +634,48 @@ class TelegramClient(TelegramBareClient):
access_hash=document.access_hash, access_hash=document.access_hash,
version=document.version version=document.version
), ),
file_path, file,
file_size=file_size, file_size=file_size,
progress_callback=progress_callback progress_callback=progress_callback
) )
return file_path return file
@staticmethod @staticmethod
def download_contact(message_media_contact, file_path, add_extension=True): def download_contact(message_media_contact, file, add_extension=True):
"""Downloads a media contact using the vCard 4.0 format""" """Downloads a media contact using the vCard 4.0 format"""
first_name = message_media_contact.first_name first_name = message_media_contact.first_name
last_name = message_media_contact.last_name last_name = message_media_contact.last_name
phone_number = message_media_contact.phone_number phone_number = message_media_contact.phone_number
# The only way we can save a contact in an understandable if isinstance(file, str):
# way by phones is by using the .vCard format # The only way we can save a contact in an understandable
if add_extension: # way by phones is by using the .vCard format
file_path += '.vcard' if add_extension:
file += '.vcard'
# Ensure that we'll be able to download the contact # Ensure that we'll be able to download the contact
utils.ensure_parent_dir_exists(file_path) utils.ensure_parent_dir_exists(file)
f = open(file, 'w', encoding='utf-8')
else:
f = file
with open(file_path, 'w', encoding='utf-8') as file: try:
file.write('BEGIN:VCARD\n') f.write('BEGIN:VCARD\n')
file.write('VERSION:4.0\n') f.write('VERSION:4.0\n')
file.write('N:{};{};;;\n'.format(first_name, last_name f.write('N:{};{};;;\n'.format(
if last_name else '')) first_name, last_name if last_name else '')
file.write('FN:{}\n'.format(' '.join((first_name, last_name))))
file.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(
phone_number))
file.write('END:VCARD\n')
return file_path
def download_file(self,
input_location,
file,
part_size_kb=None,
file_size=None,
progress_callback=None,
on_dc=None):
"""Downloads the given InputFileLocation to file (a stream or str).
If 'progress_callback' is not None, it should be a function that
takes two parameters, (bytes_downloaded, total_bytes). Note that
'total_bytes' simply equals 'file_size', and may be None.
"""
if on_dc is None:
try:
super(TelegramClient, self).download_file(
input_location,
file,
part_size_kb=part_size_kb,
file_size=file_size,
progress_callback=progress_callback
)
except FileMigrateError as e:
on_dc = e.new_dc
if on_dc is not None:
client = self._get_exported_client(on_dc)
client.download_file(
input_location,
file,
part_size_kb=part_size_kb,
file_size=file_size,
progress_callback=progress_callback
) )
f.write('FN:{}\n'.format(' '.join((first_name, last_name))))
f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(
phone_number))
f.write('END:VCARD\n')
finally:
# Only close the stream if we opened it
if isinstance(file, str):
f.close()
return file
# endregion # endregion
@ -748,7 +686,7 @@ class TelegramClient(TelegramBareClient):
def add_update_handler(self, handler): def add_update_handler(self, handler):
"""Adds an update handler (a function which takes a TLObject, """Adds an update handler (a function which takes a TLObject,
an update, as its parameter) and listens for updates""" an update, as its parameter) and listens for updates"""
if not self.sender: if not self._sender:
raise RuntimeError("You can't add update handlers until you've " raise RuntimeError("You can't add update handlers until you've "
"successfully connected to the server.") "successfully connected to the server.")
@ -771,7 +709,7 @@ class TelegramClient(TelegramBareClient):
return return
# Different state, update the saved value and behave as required # Different state, update the saved value and behave as required
self._logger.info('Changing updates thread running status to %s', running) self._logger.debug('Changing updates thread running status to %s', running)
if running: if running:
self._updates_thread_running.set() self._updates_thread_running.set()
if not self._updates_thread: if not self._updates_thread:
@ -783,7 +721,7 @@ class TelegramClient(TelegramBareClient):
else: else:
self._updates_thread_running.clear() self._updates_thread_running.clear()
if self._updates_thread_receiving.is_set(): if self._updates_thread_receiving.is_set():
self.sender.cancel_receive() self._sender.cancel_receive()
def _updates_thread_method(self): def _updates_thread_method(self):
"""This method will run until specified and listen for incoming updates""" """This method will run until specified and listen for incoming updates"""
@ -805,10 +743,14 @@ class TelegramClient(TelegramBareClient):
'Trying to receive updates from the updates thread' 'Trying to receive updates from the updates thread'
) )
updates = self.sender.receive_updates(timeout=timeout) if time() > self._next_ping_at:
self._next_ping_at = time() + self.ping_interval
self(PingRequest(utils.generate_random_long()))
updates = self._sender.receive_updates(timeout=timeout)
self._updates_thread_receiving.clear() self._updates_thread_receiving.clear()
self._logger.info( self._logger.debug(
'Received {} update(s) from the updates thread' 'Received {} update(s) from the updates thread'
.format(len(updates)) .format(len(updates))
) )
@ -817,28 +759,28 @@ class TelegramClient(TelegramBareClient):
handler(update) handler(update)
except ConnectionResetError: except ConnectionResetError:
self._logger.info('Server disconnected us. Reconnecting...') self._logger.debug('Server disconnected us. Reconnecting...')
self.reconnect() self.reconnect()
except TimeoutError: except TimeoutError:
self._logger.debug('Receiving updates timed out') self._logger.debug('Receiving updates timed out')
except ReadCancelledError: except ReadCancelledError:
self._logger.info('Receiving updates cancelled') self._logger.debug('Receiving updates cancelled')
except BrokenPipeError: except BrokenPipeError:
self._logger.info('Tcp session is broken. Reconnecting...') self._logger.debug('Tcp session is broken. Reconnecting...')
self.reconnect() self.reconnect()
except InvalidChecksumError: except InvalidChecksumError:
self._logger.info('MTProto session is broken. Reconnecting...') self._logger.debug('MTProto session is broken. Reconnecting...')
self.reconnect() self.reconnect()
except OSError: except OSError:
self._logger.warning('OSError on updates thread, %s logging out', self._logger.debug('OSError on updates thread, %s logging out',
'was' if self.sender.logging_out else 'was not') 'was' if self._sender.logging_out else 'was not')
if self.sender.logging_out: if self._sender.logging_out:
# This error is okay when logging out, means we got disconnected # This error is okay when logging out, means we got disconnected
# TODO Not sure why this happens because we call disconnect()... # TODO Not sure why this happens because we call disconnect()...
self._set_updates_thread(running=False) self._set_updates_thread(running=False)

View File

@ -1,2 +1,2 @@
from .mtproto_request import MTProtoRequest from .tlobject import TLObject
from .session import Session, JsonSession from .session import Session, JsonSession

View File

@ -1,41 +0,0 @@
from datetime import datetime, timedelta
class MTProtoRequest:
def __init__(self):
self.sent = False
self.request_msg_id = 0 # Long
self.sequence = 0
self.dirty = False
self.send_time = None
self.confirm_received = False
# These should be overrode
self.constructor_id = 0
self.confirmed = False
self.responded = False
# These should not be overrode
def on_send_success(self):
self.send_time = datetime.now()
self.sent = True
def on_confirm(self):
self.confirm_received = True
def need_resend(self):
return self.dirty or (
self.confirmed and not self.confirm_received and
datetime.now() - self.send_time > timedelta(seconds=3))
# These should be overrode
def on_send(self, writer):
pass
def on_response(self, reader):
pass
def on_exception(self, exception):
pass

View File

@ -2,7 +2,6 @@ import json
import os import os
import pickle import pickle
import platform import platform
import random
import time import time
from threading import Lock from threading import Lock
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
@ -65,15 +64,10 @@ class Session:
return self.sequence * 2 return self.sequence * 2
def get_new_msg_id(self): def get_new_msg_id(self):
"""Generates a new message ID based on the current time (in ms) since epoch""" now = time.time()
# Refer to mtproto_plain_sender.py for the original method, this is a simple copy nanoseconds = int((now - int(now)) * 1e+9)
ms_time = int(time.time() * 1000) # "message identifiers are divisible by 4"
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32) new_msg_id = (int(now) << 32) | (nanoseconds << 2)
| # "must approximately equal unix time*2^32"
((ms_time % 1000) << 22)
| # "approximate moment in time the message was created"
random.randint(0, 524288)
<< 2) # "message identifiers are divisible by 4"
if self.last_message_id >= new_msg_id: if self.last_message_id >= new_msg_id:
new_msg_id = self.last_message_id + 4 new_msg_id = self.last_message_id + 4
@ -113,14 +107,19 @@ class JsonSession:
self.system_version = session.system_version self.system_version = session.system_version
self.app_version = session.app_version self.app_version = session.app_version
self.lang_code = session.lang_code self.lang_code = session.lang_code
self.system_lang_code = session.system_lang_code
self.lang_pack = session.lang_pack
else: # str / None else: # str / None
self.session_user_id = session_user_id self.session_user_id = session_user_id
self.device_model = platform.node() system = platform.uname()
self.system_version = platform.system() self.device_model = system.system if system.system else 'Unknown'
self.app_version = '1.0' # note: '0' will provoke error self.system_version = system.release if system.release else '1.0'
self.app_version = '1.0' # '0' will provoke error
self.lang_code = 'en' self.lang_code = 'en'
self.system_lang_code = self.lang_code
self.lang_pack = ''
# Cross-thread safety # Cross-thread safety
self._lock = Lock() self._lock = Lock()
@ -133,7 +132,7 @@ class JsonSession:
self._sequence = 0 self._sequence = 0
self.salt = 0 # Unsigned long self.salt = 0 # Unsigned long
self.time_offset = 0 self.time_offset = 0
self.last_message_id = 0 # Long self._last_msg_id = 0 # Long
def save(self): def save(self):
"""Saves the current session object as session_user_id.session""" """Saves the current session object as session_user_id.session"""
@ -229,19 +228,18 @@ class JsonSession:
def get_new_msg_id(self): def get_new_msg_id(self):
"""Generates a new unique message ID based on the current """Generates a new unique message ID based on the current
time (in ms) since epoch""" time (in ms) since epoch"""
# Refer to mtproto_plain_sender.py for the original method, # Refer to mtproto_plain_sender.py for the original method
ms_time = int(time.time() * 1000) now = time.time()
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32) nanoseconds = int((now - int(now)) * 1e+9)
| # "must approximately equal unix time*2^32" # "message identifiers are divisible by 4"
((ms_time % 1000) << 22) new_msg_id = (int(now) << 32) | (nanoseconds << 2)
| # "approximate moment in time the message was created"
random.randint(0, 524288)
<< 2) # "message identifiers are divisible by 4"
if self.last_message_id >= new_msg_id: with self._lock:
new_msg_id = self.last_message_id + 4 if self._last_msg_id >= new_msg_id:
new_msg_id = self._last_msg_id + 4
self._last_msg_id = new_msg_id
self.last_message_id = new_msg_id
return new_msg_id return new_msg_id
def update_time_offset(self, correct_msg_id): def update_time_offset(self, correct_msg_id):

113
telethon/tl/tlobject.py Normal file
View File

@ -0,0 +1,113 @@
from datetime import datetime, timedelta
class TLObject:
def __init__(self):
self.sent = False
self.request_msg_id = 0 # Long
self.sequence = 0
self.dirty = False
self.send_time = None
self.confirm_received = False
# These should be overrode
self.constructor_id = 0
self.content_related = False # Only requests/functions/queries are
self.responded = False
# These should not be overrode
def on_send_success(self):
self.send_time = datetime.now()
self.sent = True
def on_confirm(self):
self.confirm_received = True
def need_resend(self):
return self.dirty or (
self.content_related and not self.confirm_received and
datetime.now() - self.send_time > timedelta(seconds=3))
@staticmethod
def pretty_format(obj, indent=None):
"""Pretty formats the given object as a string which is returned.
If indent is None, a single line will be returned.
"""
if indent is None:
if isinstance(obj, TLObject):
return '{{{}: {}}}'.format(
type(obj).__name__,
TLObject.pretty_format(obj.to_dict())
)
if isinstance(obj, dict):
return '{{{}}}'.format(', '.join(
'{}: {}'.format(
k, TLObject.pretty_format(v)
) for k, v in obj.items()
))
elif isinstance(obj, str):
return '"{}"'.format(obj)
elif hasattr(obj, '__iter__'):
return '[{}]'.format(
', '.join(TLObject.pretty_format(x) for x in obj)
)
else:
return str(obj)
else:
result = []
if isinstance(obj, TLObject):
result.append('{')
result.append(type(obj).__name__)
result.append(': ')
result.append(TLObject.pretty_format(
obj.to_dict(), indent
))
elif isinstance(obj, dict):
result.append('{\n')
indent += 1
for k, v in obj.items():
result.append('\t' * indent)
result.append(k)
result.append(': ')
result.append(TLObject.pretty_format(v, indent))
result.append(',\n')
indent -= 1
result.append('\t' * indent)
result.append('}')
elif isinstance(obj, str):
result.append('"')
result.append(obj)
result.append('"')
elif hasattr(obj, '__iter__'):
result.append('[\n')
indent += 1
for x in obj:
result.append('\t' * indent)
result.append(TLObject.pretty_format(x, indent))
result.append(',\n')
indent -= 1
result.append('\t' * indent)
result.append(']')
else:
result.append(str(obj))
return ''.join(result)
# These should be overrode
def to_dict(self):
return {}
def on_send(self, writer):
pass
def on_response(self, reader):
pass
def on_exception(self, exception):
pass

View File

@ -7,7 +7,8 @@ from mimetypes import add_type, guess_extension
from .tl.types import ( from .tl.types import (
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
InputPeerSelf, MessageMediaDocument, MessageMediaPhoto, PeerChannel, MessageMediaDocument, MessageMediaPhoto, PeerChannel, InputChannel,
UserEmpty, InputUser, InputUserEmpty, InputUserSelf, InputPeerSelf,
PeerChat, PeerUser, User, UserFull, UserProfilePhoto) PeerChat, PeerUser, User, UserFull, UserProfilePhoto)
@ -52,11 +53,14 @@ def get_extension(media):
def get_input_peer(entity): def get_input_peer(entity):
"""Gets the input peer for the given "entity" (user, chat or channel). """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 ValueError is raised if the given entity isn't a supported type."""
if type(entity).subclass_of_id == 0xc91c90b6: # crc32('InputUser') if type(entity).subclass_of_id == 0xc91c90b6: # crc32(b'InputPeer')
return entity return entity
if isinstance(entity, User): if isinstance(entity, User):
return InputPeerUser(entity.id, entity.access_hash) if entity.is_self:
return InputPeerSelf()
else:
return InputPeerUser(entity.id, entity.access_hash)
if any(isinstance(entity, c) for c in ( if any(isinstance(entity, c) for c in (
Chat, ChatEmpty, ChatForbidden)): Chat, ChatEmpty, ChatForbidden)):
@ -67,16 +71,64 @@ def get_input_peer(entity):
return InputPeerChannel(entity.id, entity.access_hash) return InputPeerChannel(entity.id, entity.access_hash)
# Less common cases # Less common cases
if isinstance(entity, UserEmpty):
return InputPeerEmpty()
if isinstance(entity, InputUser):
return InputPeerUser(entity.user_id, entity.access_hash)
if isinstance(entity, UserFull): if isinstance(entity, UserFull):
return InputPeerUser(entity.user.id, entity.user.access_hash) return get_input_peer(entity.user)
if isinstance(entity, ChatFull): if isinstance(entity, ChatFull):
return InputPeerChat(entity.id) return InputPeerChat(entity.id)
if isinstance(entity, PeerChat):
return InputPeerChat(entity.chat_id)
raise ValueError('Cannot cast {} to any kind of InputPeer.' raise ValueError('Cannot cast {} to any kind of InputPeer.'
.format(type(entity).__name__)) .format(type(entity).__name__))
def get_input_channel(entity):
"""Similar to get_input_peer, but for InputChannel's alone"""
if type(entity).subclass_of_id == 0x40f202fd: # crc32(b'InputChannel')
return entity
if isinstance(entity, Channel) or isinstance(entity, ChannelForbidden):
return InputChannel(entity.id, entity.access_hash)
if isinstance(entity, InputPeerChannel):
return InputChannel(entity.channel_id, entity.access_hash)
raise ValueError('Cannot cast {} to any kind of InputChannel.'
.format(type(entity).__name__))
def get_input_user(entity):
"""Similar to get_input_peer, but for InputUser's alone"""
if type(entity).subclass_of_id == 0xe669bf46: # crc32(b'InputUser')
return entity
if isinstance(entity, User):
if entity.is_self:
return InputUserSelf()
else:
return InputUser(entity.id, entity.access_hash)
if isinstance(entity, UserEmpty):
return InputUserEmpty()
if isinstance(entity, UserFull):
return get_input_user(entity.user)
if isinstance(entity, InputPeerUser):
return InputUser(entity.user_id, entity.access_hash)
raise ValueError('Cannot cast {} to any kind of InputUser.'
.format(type(entity).__name__))
def find_user_or_chat(peer, users, chats): def find_user_or_chat(peer, users, chats):
"""Finds the corresponding user or chat given a peer. """Finds the corresponding user or chat given a peer.
Returns None if it was not found""" Returns None if it was not found"""

View File

@ -217,7 +217,7 @@ class InteractiveTelegramClient(TelegramClient):
# Send chat message (if any) # Send chat message (if any)
elif msg: elif msg:
self.send_message( self.send_message(
entity, msg, no_web_page=True) entity, msg, link_preview=False)
def send_photo(self, path, entity): def send_photo(self, path, entity):
print('Uploading {}...'.format(path)) print('Uploading {}...'.format(path))
@ -250,7 +250,7 @@ class InteractiveTelegramClient(TelegramClient):
print('Downloading media with name {}...'.format(output)) print('Downloading media with name {}...'.format(output))
output = self.download_msg_media( output = self.download_msg_media(
msg.media, msg.media,
file_path=output, file=output,
progress_callback=self.download_progress_callback) progress_callback=self.download_progress_callback)
print('Media downloaded to {}!'.format(output)) print('Media downloaded to {}!'.format(output))
@ -275,7 +275,7 @@ class InteractiveTelegramClient(TelegramClient):
@staticmethod @staticmethod
def update_handler(update_object): def update_handler(update_object):
if type(update_object) is UpdateShortMessage: if isinstance(update_object, UpdateShortMessage):
if update_object.out: if update_object.out:
sprint('You sent {} to user #{}'.format( sprint('You sent {} to user #{}'.format(
update_object.message, update_object.user_id)) update_object.message, update_object.user_id))
@ -283,7 +283,7 @@ class InteractiveTelegramClient(TelegramClient):
sprint('[User #{} sent {}]'.format( sprint('[User #{} sent {}]'.format(
update_object.user_id, update_object.message)) update_object.user_id, update_object.message))
elif type(update_object) is UpdateShortChatMessage: elif isinstance(update_object, UpdateShortChatMessage):
if update_object.out: if update_object.out:
sprint('You sent {} to chat #{}'.format( sprint('You sent {} to chat #{}'.format(
update_object.message, update_object.chat_id)) update_object.message, update_object.chat_id))

View File

@ -1,4 +1,5 @@
import re import re
from zlib import crc32
class TLObject: class TLObject:
@ -24,12 +25,18 @@ class TLObject:
self.namespace = None self.namespace = None
self.name = fullname self.name = fullname
# The ID should be an hexadecimal string
self.id = int(object_id, base=16)
self.args = args self.args = args
self.result = result self.result = result
self.is_function = is_function self.is_function = is_function
# The ID should be an hexadecimal string or None to be inferred
if object_id is None:
self.id = self.infer_id()
else:
self.id = int(object_id, base=16)
assert self.id == self.infer_id(),\
'Invalid inferred ID for ' + repr(self)
@staticmethod @staticmethod
def from_tl(tl, is_function): def from_tl(tl, is_function):
"""Returns a TL object from the given TL scheme line""" """Returns a TL object from the given TL scheme line"""
@ -38,8 +45,10 @@ class TLObject:
match = re.match(r''' match = re.match(r'''
^ # We want to match from the beginning to the end ^ # We want to match from the beginning to the end
([\w.]+) # The .tl object can contain alpha_name or namespace.alpha_name ([\w.]+) # The .tl object can contain alpha_name or namespace.alpha_name
\# # After the name, comes the ID of the object (?:
([0-9a-f]+) # The constructor ID is in hexadecimal form \# # After the name, comes the ID of the object
([0-9a-f]+) # The constructor ID is in hexadecimal form
)? # If no constructor ID was given, CRC32 the 'tl' to determine it
(?:\s # After that, we want to match its arguments (name:type) (?:\s # After that, we want to match its arguments (name:type)
{? # For handling the start of the '{X:Type}' case {? # For handling the start of the '{X:Type}' case
@ -91,16 +100,39 @@ class TLObject:
(and thus should be embedded in the generated code) or not""" (and thus should be embedded in the generated code) or not"""
return self.id in TLObject.CORE_TYPES return self.id in TLObject.CORE_TYPES
def __repr__(self): def __repr__(self, ignore_id=False):
fullname = ('{}.{}'.format(self.namespace, self.name) fullname = ('{}.{}'.format(self.namespace, self.name)
if self.namespace is not None else self.name) if self.namespace is not None else self.name)
hex_id = hex(self.id)[2:].rjust(8, if getattr(self, 'id', None) is None or ignore_id:
'0') # Skip 0x and add 0's for padding hex_id = ''
else:
# Skip 0x and add 0's for padding
hex_id = '#' + hex(self.id)[2:].rjust(8, '0')
return '{}#{} {} = {}'.format( if self.args:
fullname, hex_id, ' '.join([str(arg) for arg in self.args]), args = ' ' + ' '.join([repr(arg) for arg in self.args])
self.result) else:
args = ''
return '{}{}{} = {}'.format(fullname, hex_id, args, self.result)
def infer_id(self):
representation = self.__repr__(ignore_id=True)
# Clean the representation
representation = representation\
.replace(':bytes ', ':string ')\
.replace('?bytes ', '?string ')\
.replace('<', ' ').replace('>', '')\
.replace('{', '').replace('}', '')
representation = re.sub(
r' \w+:flags\.\d+\?true',
r'',
representation
)
return crc32(representation.encode('ascii'))
def __str__(self): def __str__(self):
fullname = ('{}.{}'.format(self.namespace, self.name) fullname = ('{}.{}'.format(self.namespace, self.name)
@ -214,3 +246,9 @@ class TLArg:
return '{{{}:{}}}'.format(self.name, real_type) return '{{{}:{}}}'.format(self.name, real_type)
else: else:
return '{}:{}'.format(self.name, real_type) return '{}:{}'.format(self.name, real_type)
def __repr__(self):
# Get rid of our special type
return str(self)\
.replace(':date', ':int')\
.replace('?date', '?int')

View File

@ -106,6 +106,9 @@ new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long =
http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait; http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait;
ipPort ipv4:int port:int = IpPort;
help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector<ipPort> = help.ConfigSimple;
---functions--- ---functions---
rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer; rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;
@ -152,17 +155,16 @@ inputFile#f52ff27f id:long parts:int name:string md5_checksum:string = InputFile
inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile; inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile;
inputMediaEmpty#9664f57f = InputMedia; inputMediaEmpty#9664f57f = InputMedia;
inputMediaUploadedPhoto#630c9af1 flags:# file:InputFile caption:string stickers:flags.0?Vector<InputDocument> = InputMedia; inputMediaUploadedPhoto#2f37e231 flags:# file:InputFile caption:string stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
inputMediaPhoto#e9bfb4f3 id:InputPhoto caption:string = InputMedia; inputMediaPhoto#81fa373a flags:# id:InputPhoto caption:string ttl_seconds:flags.0?int = InputMedia;
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia; inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia;
inputMediaUploadedDocument#d070f1e9 flags:# file:InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> = InputMedia; inputMediaUploadedDocument#e39621fd flags:# file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
inputMediaUploadedThumbDocument#50d88cae flags:# file:InputFile thumb:InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> = InputMedia; inputMediaDocument#5acb668e flags:# id:InputDocument caption:string ttl_seconds:flags.0?int = InputMedia;
inputMediaDocument#1a77f29c id:InputDocument caption:string = InputMedia;
inputMediaVenue#2827a81a geo_point:InputGeoPoint title:string address:string provider:string venue_id:string = InputMedia; inputMediaVenue#2827a81a geo_point:InputGeoPoint title:string address:string provider:string venue_id:string = InputMedia;
inputMediaGifExternal#4843b0fd url:string q:string = InputMedia; inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
inputMediaPhotoExternal#b55f4f18 url:string caption:string = InputMedia; inputMediaPhotoExternal#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
inputMediaDocumentExternal#e5e9607c url:string caption:string = InputMedia; inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
inputMediaGame#d33f43f3 id:InputGame = 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#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia;
@ -216,11 +218,11 @@ userStatusLastMonth#77ebc742 = UserStatus;
chatEmpty#9ba2d800 id:int = Chat; chatEmpty#9ba2d800 id:int = Chat;
chat#d91cdd54 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true admins_enabled:flags.3?true admin:flags.4?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel = Chat; chat#d91cdd54 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true admins_enabled:flags.3?true admin:flags.4?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel = Chat;
chatForbidden#7328bdb id:int title:string = Chat; chatForbidden#7328bdb id:int title:string = Chat;
channel#a14dca52 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true editor:flags.3?true moderator:flags.4?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string = Chat; channel#cb44b1c flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights = Chat;
channelForbidden#8537784f flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string = Chat; channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat;
chatFull#2e02a614 id:int participants:ChatParticipants chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> = ChatFull; chatFull#2e02a614 id:int participants:ChatParticipants chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> = ChatFull;
channelFull#c3d5512f flags:# can_view_participants:flags.3?true can_set_username:flags.6?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int = ChatFull; channelFull#95cb5f57 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int = ChatFull;
chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant; chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant;
chatParticipantCreator#da13538a user_id:int = ChatParticipant; chatParticipantCreator#da13538a user_id:int = ChatParticipant;
@ -233,15 +235,15 @@ chatPhotoEmpty#37c1011c = ChatPhoto;
chatPhoto#6153276a photo_small:FileLocation photo_big:FileLocation = ChatPhoto; chatPhoto#6153276a photo_small:FileLocation photo_big:FileLocation = ChatPhoto;
messageEmpty#83e5de54 id:int = Message; messageEmpty#83e5de54 id:int = Message;
message#c09be45f flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int = Message; message#90dddc11 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string = Message;
messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message; messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message;
messageMediaEmpty#3ded6320 = MessageMedia; messageMediaEmpty#3ded6320 = MessageMedia;
messageMediaPhoto#3d8ce53d photo:Photo caption:string = MessageMedia; messageMediaPhoto#b5223b0f flags:# photo:flags.0?Photo caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia;
messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia;
messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:string user_id:int = MessageMedia; messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:string user_id:int = MessageMedia;
messageMediaUnsupported#9f84f49e = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia;
messageMediaDocument#f3e02ea8 document:Document caption:string = MessageMedia; messageMediaDocument#7c4414d3 flags:# document:flags.0?Document caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia;
messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia;
messageMediaVenue#7912b71f geo:GeoPoint title:string address:string provider:string venue_id:string = MessageMedia; messageMediaVenue#7912b71f geo:GeoPoint title:string address:string provider:string venue_id:string = MessageMedia;
messageMediaGame#fdb19008 game:Game = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia;
@ -264,6 +266,7 @@ messageActionGameScore#92a72876 game_id:long score:int = MessageAction;
messageActionPaymentSentMe#8f31b327 flags:# currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge = MessageAction; messageActionPaymentSentMe#8f31b327 flags:# currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge = MessageAction;
messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAction; messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAction;
messageActionPhoneCall#80e11a7f flags:# call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction; messageActionPhoneCall#80e11a7f flags:# call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction;
messageActionScreenshotTaken#4792929b = MessageAction;
dialog#66ffba14 flags:# pinned:flags.2?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage = Dialog; dialog#66ffba14 flags:# pinned:flags.2?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage = Dialog;
@ -326,7 +329,7 @@ contacts.link#3ace484c my_link:ContactLink foreign_link:ContactLink user:User =
contacts.contactsNotModified#b74ba9d2 = contacts.Contacts; contacts.contactsNotModified#b74ba9d2 = contacts.Contacts;
contacts.contacts#6f8b8cb2 contacts:Vector<Contact> users:Vector<User> = contacts.Contacts; contacts.contacts#6f8b8cb2 contacts:Vector<Contact> users:Vector<User> = contacts.Contacts;
contacts.importedContacts#ad524315 imported:Vector<ImportedContact> retry_contacts:Vector<long> users:Vector<User> = contacts.ImportedContacts; contacts.importedContacts#77d01c3b imported:Vector<ImportedContact> popular_invites:Vector<PopularContact> retry_contacts:Vector<long> users:Vector<User> = contacts.ImportedContacts;
contacts.blocked#1c138d15 blocked:Vector<ContactBlocked> users:Vector<User> = contacts.Blocked; contacts.blocked#1c138d15 blocked:Vector<ContactBlocked> users:Vector<User> = contacts.Blocked;
contacts.blockedSlice#900802a1 count:int blocked:Vector<ContactBlocked> users:Vector<User> = contacts.Blocked; contacts.blockedSlice#900802a1 count:int blocked:Vector<ContactBlocked> users:Vector<User> = contacts.Blocked;
@ -420,6 +423,8 @@ updateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Upd
updateBotShippingQuery#e0cdc940 query_id:long user_id:int payload:bytes shipping_address:PostAddress = Update; updateBotShippingQuery#e0cdc940 query_id:long user_id:int payload:bytes shipping_address:PostAddress = Update;
updateBotPrecheckoutQuery#5d2f3aa9 flags:# query_id:long user_id:int payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update; updateBotPrecheckoutQuery#5d2f3aa9 flags:# query_id:long user_id:int payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update;
updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update; updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update;
updateLangPackTooLong#10c2404b = Update;
updateLangPack#56022f4d difference:LangPackDifference = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@ -442,11 +447,11 @@ photos.photosSlice#15051f54 count:int photos:Vector<Photo> users:Vector<User> =
photos.photo#20212ca8 photo:Photo users:Vector<User> = photos.Photo; photos.photo#20212ca8 photo:Photo users:Vector<User> = photos.Photo;
upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File; upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File;
upload.fileCdnRedirect#1508485a dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes = upload.File; upload.fileCdnRedirect#ea52fe5a dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes cdn_file_hashes:Vector<CdnFileHash> = upload.File;
dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true id:int ip_address:string port:int = DcOption; dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int = DcOption;
config#cb601684 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string disabled_features:Vector<DisabledFeature> = Config; config#7feec888 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector<DisabledFeature> = Config;
nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;
@ -644,19 +649,16 @@ channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:
channelParticipant#15ebac1d user_id:int date:int = ChannelParticipant; channelParticipant#15ebac1d user_id:int date:int = ChannelParticipant;
channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelParticipant; channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelParticipant;
channelParticipantModerator#91057fef user_id:int inviter_id:int date:int = ChannelParticipant;
channelParticipantEditor#98192d61 user_id:int inviter_id:int date:int = ChannelParticipant;
channelParticipantKicked#8cc5e69a user_id:int kicked_by:int date:int = ChannelParticipant;
channelParticipantCreator#e3e2e1f9 user_id:int = ChannelParticipant; channelParticipantCreator#e3e2e1f9 user_id:int = ChannelParticipant;
channelParticipantAdmin#a82fa898 flags:# can_edit:flags.0?true user_id:int inviter_id:int promoted_by:int date:int admin_rights:ChannelAdminRights = ChannelParticipant;
channelParticipantBanned#222c1886 flags:# left:flags.0?true user_id:int kicked_by:int date:int banned_rights:ChannelBannedRights = ChannelParticipant;
channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter; channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter;
channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter; channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter;
channelParticipantsKicked#3c37bb7a = ChannelParticipantsFilter; channelParticipantsKicked#a3b54985 q:string = ChannelParticipantsFilter;
channelParticipantsBots#b0d1865b = ChannelParticipantsFilter; channelParticipantsBots#b0d1865b = ChannelParticipantsFilter;
channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter;
channelRoleEmpty#b285a0c6 = ChannelParticipantRole; channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter;
channelRoleModerator#9618d975 = ChannelParticipantRole;
channelRoleEditor#820bfe8c = ChannelParticipantRole;
channels.channelParticipants#f56ee2a8 count:int participants:Vector<ChannelParticipant> users:Vector<User> = channels.ChannelParticipants; channels.channelParticipants#f56ee2a8 count:int participants:Vector<ChannelParticipant> users:Vector<User> = channels.ChannelParticipants;
@ -697,7 +699,7 @@ messages.botResults#ccd3563d flags:# gallery:flags.0?true query_id:long next_off
exportedMessageLink#1f486803 link:string = ExportedMessageLink; exportedMessageLink#1f486803 link:string = ExportedMessageLink;
messageFwdHeader#c786ddcb flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int = MessageFwdHeader; messageFwdHeader#fadff4ac flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string = MessageFwdHeader;
auth.codeTypeSms#72a3158c = auth.CodeType; auth.codeTypeSms#72a3158c = auth.CodeType;
auth.codeTypeCall#741cd3e3 = auth.CodeType; auth.codeTypeCall#741cd3e3 = auth.CodeType;
@ -725,6 +727,7 @@ topPeerCategoryBotsInline#148677e2 = TopPeerCategory;
topPeerCategoryCorrespondents#637b7ed = TopPeerCategory; topPeerCategoryCorrespondents#637b7ed = TopPeerCategory;
topPeerCategoryGroups#bd17a14a = TopPeerCategory; topPeerCategoryGroups#bd17a14a = TopPeerCategory;
topPeerCategoryChannels#161d9628 = TopPeerCategory; topPeerCategoryChannels#161d9628 = TopPeerCategory;
topPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory;
topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector<TopPeer> = TopPeerCategoryPeers; topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector<TopPeer> = TopPeerCategoryPeers;
@ -795,9 +798,10 @@ pageBlockEmbedPost#292c7be9 url:string webpage_id:long author_photo_id:long auth
pageBlockCollage#8b31c4f items:Vector<PageBlock> caption:RichText = PageBlock; pageBlockCollage#8b31c4f items:Vector<PageBlock> caption:RichText = PageBlock;
pageBlockSlideshow#130c8963 items:Vector<PageBlock> caption:RichText = PageBlock; pageBlockSlideshow#130c8963 items:Vector<PageBlock> caption:RichText = PageBlock;
pageBlockChannel#ef1751b5 channel:Chat = PageBlock; pageBlockChannel#ef1751b5 channel:Chat = PageBlock;
pageBlockAudio#31b81a7f audio_id:long caption:RichText = PageBlock;
pagePart#8dee6c44 blocks:Vector<PageBlock> photos:Vector<Photo> videos:Vector<Document> = Page; pagePart#8e3f9ebe blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> = Page;
pageFull#d7a19d69 blocks:Vector<PageBlock> photos:Vector<Photo> videos:Vector<Document> = Page; pageFull#556ec7aa blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> = Page;
phoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason; phoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason;
phoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason; phoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason;
@ -844,6 +848,8 @@ account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPas
shippingOption#b6213cdf id:string title:string prices:Vector<LabeledPrice> = ShippingOption; shippingOption#b6213cdf id:string title:string prices:Vector<LabeledPrice> = ShippingOption;
inputStickerSetItem#ffa0a496 flags:# document:InputDocument emoji:string mask_coords:flags.0?MaskCoords = InputStickerSetItem;
inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall; inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall;
phoneCallEmpty#5366c915 id:long = PhoneCall; phoneCallEmpty#5366c915 id:long = PhoneCall;
@ -866,11 +872,48 @@ cdnPublicKey#c982eaba dc_id:int public_key:string = CdnPublicKey;
cdnConfig#5725e40a public_keys:Vector<CdnPublicKey> = CdnConfig; cdnConfig#5725e40a public_keys:Vector<CdnPublicKey> = CdnConfig;
langPackString#cad181f6 key:string value:string = LangPackString;
langPackStringPluralized#6c47ac9f flags:# key:string zero_value:flags.0?string one_value:flags.1?string two_value:flags.2?string few_value:flags.3?string many_value:flags.4?string other_value:string = LangPackString;
langPackStringDeleted#2979eeb2 key:string = LangPackString;
langPackDifference#f385c1f6 lang_code:string from_version:int version:int strings:Vector<LangPackString> = LangPackDifference;
langPackLanguage#117698f1 name:string native_name:string lang_code:string = LangPackLanguage;
channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true = ChannelAdminRights;
channelBannedRights#58cf4249 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true until_date:int = ChannelBannedRights;
channelAdminLogEventActionChangeTitle#e6dfb825 prev_value:string new_value:string = ChannelAdminLogEventAction;
channelAdminLogEventActionChangeAbout#55188a2e prev_value:string new_value:string = ChannelAdminLogEventAction;
channelAdminLogEventActionChangeUsername#6a4afc38 prev_value:string new_value:string = ChannelAdminLogEventAction;
channelAdminLogEventActionChangePhoto#b82f55c3 prev_photo:ChatPhoto new_photo:ChatPhoto = ChannelAdminLogEventAction;
channelAdminLogEventActionToggleInvites#1b7907ae new_value:Bool = ChannelAdminLogEventAction;
channelAdminLogEventActionToggleSignatures#26ae0971 new_value:Bool = ChannelAdminLogEventAction;
channelAdminLogEventActionUpdatePinned#e9e82c18 message:Message = ChannelAdminLogEventAction;
channelAdminLogEventActionEditMessage#709b2405 prev_message:Message new_message:Message = ChannelAdminLogEventAction;
channelAdminLogEventActionDeleteMessage#42e047bb message:Message = ChannelAdminLogEventAction;
channelAdminLogEventActionParticipantJoin#183040d3 = ChannelAdminLogEventAction;
channelAdminLogEventActionParticipantLeave#f89777f2 = ChannelAdminLogEventAction;
channelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticipant = ChannelAdminLogEventAction;
channelAdminLogEventActionParticipantToggleBan#e6d83d7e prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;
channelAdminLogEventActionParticipantToggleAdmin#d5676710 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;
channelAdminLogEvent#3b5a3e40 id:long date:int user_id:int action:ChannelAdminLogEventAction = ChannelAdminLogEvent;
channels.adminLogResults#ed8af74d events:Vector<ChannelAdminLogEvent> chats:Vector<Chat> users:Vector<User> = channels.AdminLogResults;
channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true = ChannelAdminLogEventsFilter;
popularContact#5ce14175 client_id:long importers:int = PopularContact;
cdnFileHash#77eec38f offset:int limit:int hash:bytes = CdnFileHash;
---functions--- ---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector<long> query:!X = X; invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector<long> query:!X = X;
initConnection#69796de9 {X:Type} api_id:int device_model:string system_version:string app_version:string lang_code:string query:!X = X; initConnection#c7481da6 {X:Type} api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string query:!X = X;
invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X; invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;
invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X; invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;
@ -935,13 +978,13 @@ contacts.exportCard#84e53737 = Vector<int>;
contacts.importCard#4fe196fe export_card:Vector<int> = User; contacts.importCard#4fe196fe export_card:Vector<int> = User;
contacts.search#11f812d8 q:string limit:int = contacts.Found; contacts.search#11f812d8 q:string limit:int = contacts.Found;
contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer;
contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:int = contacts.TopPeers; contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:int = contacts.TopPeers;
contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;
messages.getMessages#4222fa74 id:Vector<int> = messages.Messages; messages.getMessages#4222fa74 id:Vector<int> = messages.Messages;
messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs; messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs;
messages.getHistory#afa92846 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.getHistory#afa92846 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
messages.search#d4569248 flags:# peer:InputPeer q:string filter:MessagesFilter min_date:int max_date:int offset:int max_id:int limit:int = messages.Messages; messages.search#f288a275 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset:int max_id:int limit:int = messages.Messages;
messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages; messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages;
messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true peer:InputPeer max_id:int = messages.AffectedHistory; messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true peer:InputPeer max_id:int = messages.AffectedHistory;
messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages; messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages;
@ -1023,6 +1066,8 @@ messages.reorderPinnedDialogs#959ff644 flags:# force:flags.0?true order:Vector<I
messages.getPinnedDialogs#e254d64e = messages.PeerDialogs; messages.getPinnedDialogs#e254d64e = messages.PeerDialogs;
messages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector<ShippingOption> = Bool; messages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector<ShippingOption> = Bool;
messages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool; messages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool;
messages.uploadMedia#519bc2b1 peer:InputPeer media:InputMedia = MessageMedia;
messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int random_id:long = Updates;
updates.getState#edd4882a = updates.State; updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
@ -1038,7 +1083,8 @@ upload.getFile#e3a6cfb5 location:InputFileLocation offset:int limit:int = upload
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool; upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile; upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile; upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile;
upload.reuploadCdnFile#2e7a2020 file_token:bytes request_token:bytes = Bool; upload.reuploadCdnFile#1af91c09 file_token:bytes request_token:bytes = Vector<CdnFileHash>;
upload.getCdnFileHashes#f715c87b file_token:bytes offset:int = Vector<CdnFileHash>;
help.getConfig#c4f9186b = Config; help.getConfig#c4f9186b = Config;
help.getNearestDc#1fb33026 = NearestDc; help.getNearestDc#1fb33026 = NearestDc;
@ -1062,7 +1108,7 @@ channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;
channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull; channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull;
channels.createChannel#f4893d7f flags:# broadcast:flags.0?true megagroup:flags.1?true title:string about:string = Updates; channels.createChannel#f4893d7f flags:# broadcast:flags.0?true megagroup:flags.1?true title:string about:string = Updates;
channels.editAbout#13e27f1e channel:InputChannel about:string = Bool; channels.editAbout#13e27f1e channel:InputChannel about:string = Bool;
channels.editAdmin#eb7611d0 channel:InputChannel user_id:InputUser role:ChannelParticipantRole = Updates; channels.editAdmin#20b88214 channel:InputChannel user_id:InputUser admin_rights:ChannelAdminRights = Updates;
channels.editTitle#566decd0 channel:InputChannel title:string = Updates; channels.editTitle#566decd0 channel:InputChannel title:string = Updates;
channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates; channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates;
channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool; channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool;
@ -1070,7 +1116,6 @@ channels.updateUsername#3514b3de channel:InputChannel username:string = Bool;
channels.joinChannel#24b524c5 channel:InputChannel = Updates; channels.joinChannel#24b524c5 channel:InputChannel = Updates;
channels.leaveChannel#f836aa95 channel:InputChannel = Updates; channels.leaveChannel#f836aa95 channel:InputChannel = Updates;
channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector<InputUser> = Updates; channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector<InputUser> = Updates;
channels.kickFromChannel#a672de14 channel:InputChannel user_id:InputUser kicked:Bool = Updates;
channels.exportInvite#c7560885 channel:InputChannel = ExportedChatInvite; channels.exportInvite#c7560885 channel:InputChannel = ExportedChatInvite;
channels.deleteChannel#c0111fe3 channel:InputChannel = Updates; channels.deleteChannel#c0111fe3 channel:InputChannel = Updates;
channels.toggleInvites#49609307 channel:InputChannel enabled:Bool = Updates; channels.toggleInvites#49609307 channel:InputChannel enabled:Bool = Updates;
@ -1078,6 +1123,8 @@ channels.exportMessageLink#c846d22d channel:InputChannel id:int = ExportedMessag
channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates; channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates;
channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates; channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates;
channels.getAdminedPublicChannels#8d8d82d7 = messages.Chats; channels.getAdminedPublicChannels#8d8d82d7 = messages.Chats;
channels.editBanned#bfd915cd channel:InputChannel user_id:InputUser banned_rights:ChannelBannedRights = Updates;
channels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector<InputUser> max_id:long min_id:long limit:int = channels.AdminLogResults;
bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON;
bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool;
@ -1089,6 +1136,11 @@ payments.sendPaymentForm#2b8879b3 flags:# msg_id:int requested_info_id:flags.0?s
payments.getSavedInfo#227d824b = payments.SavedInfo; payments.getSavedInfo#227d824b = payments.SavedInfo;
payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool; payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool;
stickers.createStickerSet#9bd86e6a flags:# masks:flags.0?true user_id:InputUser title:string short_name:string stickers:Vector<InputStickerSetItem> = messages.StickerSet;
stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet;
stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet;
stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet;
phone.getCallConfig#55451fa9 = DataJSON; phone.getCallConfig#55451fa9 = DataJSON;
phone.requestCall#5b95b3d4 user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.requestCall#5b95b3d4 user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
@ -1098,4 +1150,9 @@ phone.discardCall#78d413a6 peer:InputPhoneCall duration:int reason:PhoneCallDisc
phone.setCallRating#1c536a34 peer:InputPhoneCall rating:int comment:string = Updates; phone.setCallRating#1c536a34 peer:InputPhoneCall rating:int comment:string = Updates;
phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool; phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool;
// LAYER 66 langpack.getLangPack#9ab5c58e lang_code:string = LangPackDifference;
langpack.getStrings#2e1ee318 lang_code:string keys:Vector<string> = Vector<LangPackString>;
langpack.getDifference#b2e4d7d from_version:int = LangPackDifference;
langpack.getLanguages#800fd57d = Vector<LangPackLanguage>;
// LAYER 70

163
telethon_generator/tl_generator.py Executable file → Normal file
View File

@ -1,51 +1,46 @@
#!/usr/bin/env python3
import os import os
import re import re
import shutil import shutil
from zlib import crc32 from zlib import crc32
from collections import defaultdict from collections import defaultdict
try: from .parser import SourceBuilder, TLParser
from .parser import SourceBuilder, TLParser
except (ImportError, SystemError):
from parser import SourceBuilder, TLParser
def get_output_path(normal_path):
return os.path.join('../telethon/tl', normal_path)
output_base_depth = 2 # telethon/tl/
class TLGenerator: class TLGenerator:
@staticmethod def __init__(self, output_dir):
def tlobjects_exist(): self.output_dir = output_dir
def _get_file(self, *paths):
return os.path.join(self.output_dir, *paths)
def _rm_if_exists(self, filename):
file = self._get_file(filename)
if os.path.exists(file):
if os.path.isdir(file):
shutil.rmtree(file)
else:
os.remove(file)
def tlobjects_exist(self):
"""Determines whether the TLObjects were previously """Determines whether the TLObjects were previously
generated (hence exist) or not generated (hence exist) or not
""" """
return os.path.isfile(get_output_path('all_tlobjects.py')) return os.path.isfile(self._get_file('all_tlobjects.py'))
@staticmethod def clean_tlobjects(self):
def clean_tlobjects():
"""Cleans the automatically generated TLObjects from disk""" """Cleans the automatically generated TLObjects from disk"""
if os.path.isdir(get_output_path('functions')): for name in ('functions', 'types', 'all_tlobjects.py'):
shutil.rmtree(get_output_path('functions')) self._rm_if_exists(name)
if os.path.isdir(get_output_path('types')): def generate_tlobjects(self, scheme_file, import_depth):
shutil.rmtree(get_output_path('types'))
if os.path.isfile(get_output_path('all_tlobjects.py')):
os.remove(get_output_path('all_tlobjects.py'))
@staticmethod
def generate_tlobjects(scheme_file):
"""Generates all the TLObjects from scheme.tl to """Generates all the TLObjects from scheme.tl to
tl/functions and tl/types tl/functions and tl/types
""" """
# First ensure that the required parent directories exist # First ensure that the required parent directories exist
os.makedirs(get_output_path('functions'), exist_ok=True) os.makedirs(self._get_file('functions'), exist_ok=True)
os.makedirs(get_output_path('types'), exist_ok=True) os.makedirs(self._get_file('types'), exist_ok=True)
# Step 0: Cache the parsed file on a tuple # Step 0: Cache the parsed file on a tuple
tlobjects = tuple(TLParser.parse_file(scheme_file)) tlobjects = tuple(TLParser.parse_file(scheme_file))
@ -91,11 +86,11 @@ class TLGenerator:
continue continue
# Determine the output directory and create it # Determine the output directory and create it
out_dir = get_output_path('functions' out_dir = self._get_file('functions'
if tlobject.is_function else 'types') if tlobject.is_function else 'types')
# Path depth to perform relative import # Path depth to perform relative import
depth = output_base_depth depth = import_depth
if tlobject.namespace: if tlobject.namespace:
depth += 1 depth += 1
out_dir = os.path.join(out_dir, tlobject.namespace) out_dir = os.path.join(out_dir, tlobject.namespace)
@ -121,19 +116,19 @@ class TLGenerator:
tlobject, builder, depth, type_constructors) tlobject, builder, depth, type_constructors)
# Step 3: Add the relative imports to the namespaces on __init__.py's # Step 3: Add the relative imports to the namespaces on __init__.py's
init_py = os.path.join(get_output_path('functions'), '__init__.py') init_py = self._get_file('functions', '__init__.py')
with open(init_py, 'a') as file: with open(init_py, 'a') as file:
file.write('from . import {}\n' file.write('from . import {}\n'
.format(', '.join(function_namespaces))) .format(', '.join(function_namespaces)))
init_py = os.path.join(get_output_path('types'), '__init__.py') init_py = self._get_file('types', '__init__.py')
with open(init_py, 'a') as file: with open(init_py, 'a') as file:
file.write('from . import {}\n' file.write('from . import {}\n'
.format(', '.join(type_namespaces))) .format(', '.join(type_namespaces)))
# Step 4: Once all the objects have been generated, # Step 4: Once all the objects have been generated,
# we can now group them in a single file # we can now group them in a single file
filename = os.path.join(get_output_path('all_tlobjects.py')) filename = os.path.join(self._get_file('all_tlobjects.py'))
with open(filename, 'w', encoding='utf-8') as file: with open(filename, 'w', encoding='utf-8') as file:
with SourceBuilder(file) as builder: with SourceBuilder(file) as builder:
builder.writeln( builder.writeln(
@ -182,17 +177,27 @@ class TLGenerator:
importing and documentation strings. importing and documentation strings.
'""" '"""
# Both types and functions inherit from # Both types and functions inherit from the TLObject class so they
# MTProtoRequest so they all can be sent # all can be serialized and sent, however, only the functions are
builder.writeln('from {}.tl.mtproto_request import MTProtoRequest' # "content_related".
builder.writeln('from {}.tl.tlobject import TLObject'
.format('.' * depth)) .format('.' * depth))
if tlobject.is_function and \ if tlobject.is_function:
any(a for a in tlobject.args if a.type == 'InputPeer'): util_imports = set()
# We can automatically convert a normal peer to an InputPeer, for a in tlobject.args:
# it will make invoking a lot of requests a lot simpler. # We can automatically convert some "full" types to
builder.writeln('from {}.utils import get_input_peer' # "input only" (like User -> InputPeerUser, etc.)
.format('.' * depth)) if a.type == 'InputPeer':
util_imports.add('get_input_peer')
elif a.type == 'InputChannel':
util_imports.add('get_input_channel')
elif a.type == 'InputUser':
util_imports.add('get_input_user')
if util_imports:
builder.writeln('from {}.utils import {}'.format(
'.' * depth, ', '.join(util_imports)))
if any(a for a in tlobject.args if a.can_be_inferred): if any(a for a in tlobject.args if a.can_be_inferred):
# Currently only 'random_id' needs 'os' to be imported # Currently only 'random_id' needs 'os' to be imported
@ -200,7 +205,7 @@ class TLGenerator:
builder.writeln() builder.writeln()
builder.writeln() builder.writeln()
builder.writeln('class {}(MTProtoRequest):'.format( builder.writeln('class {}(TLObject):'.format(
TLGenerator.get_class_name(tlobject))) TLGenerator.get_class_name(tlobject)))
# Write the original .tl definition, # Write the original .tl definition,
@ -264,7 +269,7 @@ class TLGenerator:
builder.write(' Must be a list.'.format(arg.name)) builder.write(' Must be a list.'.format(arg.name))
if arg.is_generic: if arg.is_generic:
builder.write(' Must be another MTProtoRequest.') builder.write(' Must be another TLObject request.')
builder.writeln() builder.writeln()
@ -296,7 +301,7 @@ class TLGenerator:
if tlobject.is_function: if tlobject.is_function:
builder.writeln('self.result = None') builder.writeln('self.result = None')
builder.writeln( builder.writeln(
'self.confirmed = True # Confirmed by default') 'self.content_related = True')
# Set the arguments # Set the arguments
if args: if args:
@ -317,10 +322,15 @@ class TLGenerator:
) )
else: else:
raise ValueError('Cannot infer a value for ', arg) raise ValueError('Cannot infer a value for ', arg)
# Well-known cases, auto-cast it to the right type
elif arg.type == 'InputPeer' and tlobject.is_function: elif arg.type == 'InputPeer' and tlobject.is_function:
# Well-known case, auto-cast it to the right type TLGenerator.write_get_input(builder, arg, 'get_input_peer')
builder.writeln( elif arg.type == 'InputChannel' and tlobject.is_function:
'self.{0} = get_input_peer({0})'.format(arg.name)) TLGenerator.write_get_input(builder, arg, 'get_input_channel')
elif arg.type == 'InputUser' and tlobject.is_function:
TLGenerator.write_get_input(builder, arg, 'get_input_user')
else: else:
builder.writeln('self.{0} = {0}'.format(arg.name)) builder.writeln('self.{0} = {0}'.format(arg.name))
@ -413,9 +423,28 @@ class TLGenerator:
builder.end_block() builder.end_block()
builder.writeln('def __str__(self):') builder.writeln('def __str__(self):')
builder.writeln('return {}'.format(str(tlobject))) builder.writeln('return TLObject.pretty_format(self)')
builder.end_block()
builder.writeln('def stringify(self):')
builder.writeln('return TLObject.pretty_format(self, indent=0)')
# builder.end_block() # No need to end the last block # builder.end_block() # No need to end the last block
@staticmethod
def write_get_input(builder, arg, get_input_code):
"""Returns "True" if the get_input_* code was written when assigning
a parameter upon creating the request. Returns False otherwise
"""
if arg.is_vector:
builder.writeln(
'self.{0} = [{1}(_x) for _x in {0}]'
.format(arg.name, get_input_code)
)
pass
else:
builder.writeln(
'self.{0} = {1}({0})'.format(arg.name, get_input_code)
)
@staticmethod @staticmethod
def get_class_name(tlobject): def get_class_name(tlobject):
@ -475,11 +504,10 @@ class TLGenerator:
"writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID") "writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID")
builder.writeln('writer.write_int(len({}))'.format(name)) builder.writeln('writer.write_int(len({}))'.format(name))
builder.writeln('for {}_item in {}:'.format(arg.name, name)) builder.writeln('for _x in {}:'.format(name))
# Temporary disable .is_vector, not to enter this if again # Temporary disable .is_vector, not to enter this if again
arg.is_vector = False arg.is_vector = False
TLGenerator.write_onsend_code( TLGenerator.write_onsend_code(builder, arg, args, name='_x')
builder, arg, args, name='{}_item'.format(arg.name))
arg.is_vector = True arg.is_vector = True
elif arg.flag_indicator: elif arg.flag_indicator:
@ -570,13 +598,12 @@ class TLGenerator:
builder.writeln("reader.read_int() # Vector's constructor ID") builder.writeln("reader.read_int() # Vector's constructor ID")
builder.writeln('{} = [] # Initialize an empty list'.format(name)) builder.writeln('{} = [] # Initialize an empty list'.format(name))
builder.writeln('{}_len = reader.read_int()'.format(arg.name)) builder.writeln('_len = reader.read_int()')
builder.writeln('for _ in range({}_len):'.format(arg.name)) builder.writeln('for _ in range(_len):')
# Temporary disable .is_vector, not to enter this if again # Temporary disable .is_vector, not to enter this if again
arg.is_vector = False arg.is_vector = False
TLGenerator.write_onresponse_code( TLGenerator.write_onresponse_code(builder, arg, args, name='_x')
builder, arg, args, name='{}_item'.format(arg.name)) builder.writeln('{}.append(_x)'.format(name))
builder.writeln('{}.append({}_item)'.format(name, arg.name))
arg.is_vector = True arg.is_vector = True
elif arg.flag_indicator: elif arg.flag_indicator:
@ -591,12 +618,14 @@ class TLGenerator:
builder.writeln('{} = reader.read_long()'.format(name)) builder.writeln('{} = reader.read_long()'.format(name))
elif 'int128' == arg.type: elif 'int128' == arg.type:
builder.writeln('{} = reader.read_large_int(bits=128)'.format( builder.writeln(
name)) '{} = reader.read_large_int(bits=128)'.format(name)
)
elif 'int256' == arg.type: elif 'int256' == arg.type:
builder.writeln('{} = reader.read_large_int(bits=256)'.format( builder.writeln(
name)) '{} = reader.read_large_int(bits=256)'.format(name)
)
elif 'double' == arg.type: elif 'double' == arg.type:
builder.writeln('{} = reader.read_double()'.format(name)) builder.writeln('{} = reader.read_double()'.format(name))
@ -658,13 +687,3 @@ class TLGenerator:
builder.writeln('self.result = reader.tgread_vector()') builder.writeln('self.result = reader.tgread_vector()')
else: else:
builder.writeln('self.result = reader.tgread_object()') builder.writeln('self.result = reader.tgread_object()')
if __name__ == '__main__':
if TLGenerator.tlobjects_exist():
print('Detected previous TLObjects. Cleaning...')
TLGenerator.clean_tlobjects()
print('Generating TLObjects...')
TLGenerator.generate_tlobjects('scheme.tl')
print('Done.')