mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-05 04:30:22 +03:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
34261f1f76
19
README.rst
19
README.rst
|
@ -58,11 +58,11 @@ On a terminal, issue the following command:
|
|||
|
||||
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
|
||||
|
||||
sudo -H pip install --upgrade telethon
|
||||
If you already have Telethon installed,
|
||||
upgrade with ``pip install --upgrade telethon``!
|
||||
|
||||
Installing Telethon manually
|
||||
----------------------------
|
||||
|
@ -71,7 +71,7 @@ Installing Telethon manually
|
|||
(`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``
|
||||
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!
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
result = client(SomeRequest(...))
|
||||
|
||||
# Or the old way:
|
||||
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/``.
|
||||
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
|
||||
`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.
|
||||
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.
|
||||
|
||||
|
|
|
@ -75,12 +75,19 @@ def get_create_path_for(tlobject):
|
|||
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='.'):
|
||||
"""Similar to getting the path for a TLObject, it might not be possible
|
||||
to have the TLObject itself but rather its name (the type);
|
||||
this method works in the same way, returning a relative path"""
|
||||
if type_.lower() in {'int', 'long', 'int128', 'int256', 'double',
|
||||
'vector', 'string', 'bool', 'true', 'bytes', 'date'}:
|
||||
if is_core_type(type_):
|
||||
path = 'index.html#%s' % type_.lower()
|
||||
|
||||
elif '.' in type_:
|
||||
|
@ -396,11 +403,39 @@ def generate_documentation(scheme_file):
|
|||
docs.add_row(get_class_name(func), link=link)
|
||||
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
|
||||
docs.write_title('Other types containing this type', level=3)
|
||||
other_types = sorted((t for t in tlobjects
|
||||
if any(tltype == a.type for a in t.args)),
|
||||
key=lambda t: t.name)
|
||||
other_types = sorted(
|
||||
(t for t in tlobjects
|
||||
if any(tltype == a.type for a in t.args)
|
||||
and not t.is_function
|
||||
), key=lambda t: t.name
|
||||
)
|
||||
|
||||
if not other_types:
|
||||
docs.write_text(
|
||||
|
@ -433,20 +468,30 @@ def generate_documentation(scheme_file):
|
|||
layer = TLParser.find_layer(scheme_file)
|
||||
types = set()
|
||||
methods = []
|
||||
constructors = []
|
||||
for tlobject in tlobjects:
|
||||
if tlobject.is_function:
|
||||
methods.append(tlobject)
|
||||
else:
|
||||
constructors.append(tlobject)
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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 = {
|
||||
'type_count': len(types),
|
||||
|
@ -456,8 +501,10 @@ def generate_documentation(scheme_file):
|
|||
|
||||
'request_names': request_names,
|
||||
'type_names': type_names,
|
||||
'constructor_names': constructor_names,
|
||||
'request_urls': request_urls,
|
||||
'type_urls': type_urls
|
||||
'type_urls': type_urls,
|
||||
'constructor_urls': constructor_urls
|
||||
}
|
||||
|
||||
with open('../res/core.html') as infile:
|
||||
|
|
|
@ -14,11 +14,26 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="main_div">
|
||||
<!-- You can append '?q=query' to the URL to default to a search -->
|
||||
<input id="searchBox" type="text" onkeyup="updateSearch()"
|
||||
placeholder="Search for requests and types…" />
|
||||
|
||||
<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 id="contentDiv">
|
||||
|
@ -146,7 +161,6 @@
|
|||
<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.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>
|
||||
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>]
|
||||
|
||||
<span class="sh3"># <b>(4)</b> !! Invoking a request manually !!</span>
|
||||
result = <b>client.invoke</b>(
|
||||
GetHistoryRequest(
|
||||
get_input_peer(entity),
|
||||
result = <b>client</b>(GetHistoryRequest(
|
||||
entity,
|
||||
limit=<span class="sh1">20</span>,
|
||||
offset_date=<span class="sh1">None</span>,
|
||||
offset_id=<span class="sh1">0</span>,
|
||||
max_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>
|
||||
messages = result.messages</pre>
|
||||
|
||||
<p>As it can be seen, manually invoking requests with
|
||||
<code>client.invoke()</code> is way more verbose than using the built-in
|
||||
methods (such as <code>client.get_dialogs()</code>. 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>
|
||||
<p>As it can be seen, manually calling requests with
|
||||
<code>client(request)</code> (or using the old way, by calling
|
||||
<code>client.invoke(request)</code>) is way more verbose than using the
|
||||
built-in methods (such as <code>client.get_dialogs()</code>).</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>
|
||||
|
@ -193,13 +210,66 @@ messages = result.messages</pre>
|
|||
contentDiv = document.getElementById("contentDiv");
|
||||
searchDiv = document.getElementById("searchDiv");
|
||||
searchBox = document.getElementById("searchBox");
|
||||
searchTable = document.getElementById("searchTable");
|
||||
|
||||
requests = [{request_names}];
|
||||
types = [{type_names}];
|
||||
// Search lists
|
||||
methodsList = document.getElementById("methodsList");
|
||||
methodsCount = document.getElementById("methodsCount");
|
||||
|
||||
requestsu = [{request_urls}];
|
||||
typesu = [{type_urls}];
|
||||
typesList = document.getElementById("typesList");
|
||||
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() {
|
||||
if (searchBox.value) {
|
||||
|
@ -207,52 +277,37 @@ function updateSearch() {
|
|||
searchDiv.style.display = "";
|
||||
|
||||
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 foundTypesu = [];
|
||||
for (var i = 0; i < types.length; ++i) {
|
||||
if (types[i].toLowerCase().indexOf(query) != -1) {
|
||||
foundTypes.push(types[i]);
|
||||
foundTypesu.push(typesu[i]);
|
||||
}
|
||||
}
|
||||
var foundRequests = getSearchArray(requests, requestsu, query);
|
||||
var foundTypes = getSearchArray(types, typesu, query);
|
||||
var foundConstructors = getSearchArray(
|
||||
constructors, constructorsu, query
|
||||
);
|
||||
|
||||
var top = foundRequests.length > foundTypes.length ?
|
||||
foundRequests.length : foundTypes.length;
|
||||
|
||||
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;
|
||||
buildList(methodsCount, methodsList, foundRequests);
|
||||
buildList(typesCount, typesList, foundTypes);
|
||||
buildList(constructorsCount, constructorsList, foundConstructors);
|
||||
} else {
|
||||
contentDiv.style.display = "";
|
||||
searchDiv.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function getQuery(name) {
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i = 0; i != vars.length; ++i) {
|
||||
var pair = vars[i].split("=");
|
||||
if (pair[0] == name)
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
|
||||
var query = getQuery('q');
|
||||
if (query) {
|
||||
searchBox.value = query;
|
||||
}
|
||||
|
||||
updateSearch();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -52,7 +52,7 @@ table td {
|
|||
margin: 0 8px -2px 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
h1, summary.title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
|
@ -137,6 +137,26 @@ button:hover {
|
|||
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 */
|
||||
.invisible {
|
||||
left: 0;
|
||||
|
@ -153,7 +173,7 @@ button:hover {
|
|||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
h1 {
|
||||
h1, summary.title {
|
||||
font-size: 18px;
|
||||
}
|
||||
h3 {
|
||||
|
|
77
setup.py
Normal file → Executable file
77
setup.py
Normal file → Executable file
|
@ -1,87 +1,98 @@
|
|||
#!/usr/bin/env python3
|
||||
"""A setuptools based setup module.
|
||||
|
||||
See:
|
||||
https://packaging.python.org/en/latest/distributing.html
|
||||
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
|
||||
from codecs import open
|
||||
from sys import argv
|
||||
from os import path
|
||||
|
||||
# Always prefer setuptools over distutils
|
||||
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
|
||||
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
|
||||
if __name__ == '__main__':
|
||||
if len(argv) >= 2 and argv[1] == 'gen_tl':
|
||||
from telethon_generator.tl_generator import TLGenerator
|
||||
generator = TLGenerator('telethon/tl')
|
||||
if generator.tlobjects_exist():
|
||||
print('Detected previous TLObjects. Cleaning...')
|
||||
generator.clean_tlobjects()
|
||||
|
||||
print('Generating TLObjects...')
|
||||
generator.generate_tlobjects(
|
||||
'telethon_generator/scheme.tl', import_depth=2
|
||||
)
|
||||
print('Done.')
|
||||
|
||||
elif len(argv) >= 2 and argv[1] == 'clean_tl':
|
||||
from telethon_generator.tl_generator import TLGenerator
|
||||
print('Cleaning...')
|
||||
TLGenerator('telethon/tl').clean_tlobjects()
|
||||
print('Done.')
|
||||
|
||||
else:
|
||||
if not TelegramClient:
|
||||
print('Run `python3', argv[0], 'gen_tl` first.')
|
||||
quit()
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
# Get the long description from the README file
|
||||
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
setup(
|
||||
name='Telethon',
|
||||
|
||||
# Versions should comply with PEP440.
|
||||
version=TelegramClient.__version__,
|
||||
description="Python3 Telegram's client implementation with full access to its API",
|
||||
description="Full-featured Telegram client library for Python 3",
|
||||
long_description=long_description,
|
||||
|
||||
# The project's main homepage.
|
||||
url='https://github.com/LonamiWebs/Telethon',
|
||||
download_url='https://github.com/LonamiWebs/Telethon/releases',
|
||||
|
||||
# Author details
|
||||
author='Lonami Exo',
|
||||
author_email='totufals@hotmail.com',
|
||||
|
||||
# Choose your license
|
||||
license='MIT',
|
||||
|
||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
# 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
|
||||
'Intended Audience :: Developers',
|
||||
'Topic :: Communications :: Chat',
|
||||
|
||||
# Pick your license as you wish (should match "license" above)
|
||||
'License :: OSI Approved :: MIT License',
|
||||
|
||||
# Specify the Python versions you support here. In particular, ensure
|
||||
# that you indicate whether you support Python 2, Python 3 or both.
|
||||
'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?
|
||||
keywords='Telegram API chat client MTProto',
|
||||
|
||||
# You can just specify the packages manually here if your project is
|
||||
# simple. Or you can use find_packages().
|
||||
keywords='telegram api chat client library messaging mtproto',
|
||||
packages=find_packages(exclude=[
|
||||
'telethon_generator', 'telethon_tests', 'run_tests.py',
|
||||
'try_telethon.py'
|
||||
]),
|
||||
|
||||
# List run-time dependencies here. These will be installed by pip when
|
||||
# your project is installed.
|
||||
install_requires=['pyaes'],
|
||||
|
||||
# To provide executable scripts, use entry points in preference to the
|
||||
# "scripts" keyword. Entry points provide cross-platform support and allow
|
||||
# pip to create the appropriate form of executable for the target platform.
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'gen_tl = tl_generator:clean_and_generate',
|
||||
],
|
||||
})
|
||||
install_requires=['pyaes']
|
||||
)
|
||||
|
|
|
@ -18,10 +18,10 @@ from .rpc_errors_420 import *
|
|||
|
||||
def rpc_message_to_error(code, message):
|
||||
errors = {
|
||||
303: rpc_303_errors,
|
||||
400: rpc_400_errors,
|
||||
401: rpc_401_errors,
|
||||
420: rpc_420_errors
|
||||
303: rpc_errors_303_all,
|
||||
400: rpc_errors_400_all,
|
||||
401: rpc_errors_401_all,
|
||||
420: rpc_errors_420_all
|
||||
}.get(code, None)
|
||||
|
||||
if errors is not None:
|
||||
|
|
|
@ -43,7 +43,7 @@ class UserMigrateError(InvalidDCError):
|
|||
)
|
||||
|
||||
|
||||
rpc_303_errors = {
|
||||
rpc_errors_303_all = {
|
||||
'FILE_MIGRATE_(\d+)': FileMigrateError,
|
||||
'PHONE_MIGRATE_(\d+)': PhoneMigrateError,
|
||||
'NETWORK_MIGRATE_(\d+)': NetworkMigrateError,
|
||||
|
|
|
@ -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):
|
||||
def __init__(self, **kwargs):
|
||||
super(Exception, self).__init__(
|
||||
|
@ -321,7 +329,7 @@ class UserIdInvalidError(BadRequestError):
|
|||
)
|
||||
|
||||
|
||||
rpc_400_errors = {
|
||||
rpc_errors_400_all = {
|
||||
'API_ID_INVALID': ApiIdInvalidError,
|
||||
'BOT_METHOD_INVALID': BotMethodInvalidError,
|
||||
'CHANNEL_INVALID': ChannelInvalidError,
|
||||
|
|
|
@ -84,7 +84,7 @@ class UserDeactivatedError(UnauthorizedError):
|
|||
)
|
||||
|
||||
|
||||
rpc_401_errors = {
|
||||
rpc_errors_401_all = {
|
||||
'ACTIVE_USER_REQUIRED': ActiveUserRequiredError,
|
||||
'AUTH_KEY_INVALID': AuthKeyInvalidError,
|
||||
'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError,
|
||||
|
|
|
@ -11,6 +11,6 @@ class FloodWaitError(FloodError):
|
|||
)
|
||||
|
||||
|
||||
rpc_420_errors = {
|
||||
rpc_errors_420_all = {
|
||||
'FLOOD_WAIT_(\d+)': FloodWaitError
|
||||
}
|
||||
|
|
|
@ -30,9 +30,12 @@ class TcpClient:
|
|||
else: # tuple, list, etc.
|
||||
self._socket.set_proxy(*self._proxy)
|
||||
|
||||
def connect(self, ip, port):
|
||||
"""Connects to the specified IP and port number"""
|
||||
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
|
||||
|
@ -80,7 +83,7 @@ class TcpClient:
|
|||
|
||||
# Set the starting time so we can
|
||||
# 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:
|
||||
bytes_left = size
|
||||
|
@ -93,8 +96,9 @@ class TcpClient:
|
|||
try:
|
||||
partial = self._socket.recv(bytes_left)
|
||||
if len(partial) == 0:
|
||||
self.connected = False
|
||||
raise ConnectionResetError(
|
||||
'The server has closed the connection (recv() returned 0 bytes).')
|
||||
'The server has closed the connection.')
|
||||
|
||||
buffer.write(partial)
|
||||
bytes_left -= len(partial)
|
||||
|
@ -104,7 +108,7 @@ class TcpClient:
|
|||
time.sleep(self.delay)
|
||||
|
||||
# Check if the timeout finished
|
||||
if timeout:
|
||||
if timeout is not None:
|
||||
time_passed = datetime.now() - start_time
|
||||
if time_passed > timeout:
|
||||
raise TimeoutError(
|
||||
|
|
94
telethon/extensions/threaded_tcp_client.py
Normal file
94
telethon/extensions/threaded_tcp_client.py
Normal 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()
|
|
@ -1,4 +1,3 @@
|
|||
import random
|
||||
import time
|
||||
|
||||
from ..extensions import BinaryReader, BinaryWriter
|
||||
|
@ -42,17 +41,12 @@ class MtProtoPlainSender:
|
|||
return response
|
||||
|
||||
def _get_new_msg_id(self):
|
||||
"""Generates a new message ID based on the current time (in ms) since epoch"""
|
||||
# See https://core.telegram.org/mtproto/description#message-identifier-msg-id
|
||||
ms_time = int(time.time() * 1000)
|
||||
new_msg_id = (((ms_time // 1000) << 32)
|
||||
| # "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"
|
||||
|
||||
# Ensure that we always return a message ID which is higher than the previous one
|
||||
"""Generates a new message ID based on the current time since epoch"""
|
||||
# See core.telegram.org/mtproto/description#message-identifier-msg-id
|
||||
now = time.time()
|
||||
nanoseconds = int((now - int(now)) * 1e+9)
|
||||
# "message identifiers are divisible by 4"
|
||||
new_msg_id = (int(now) << 32) | (nanoseconds << 2)
|
||||
if self._last_msg_id >= new_msg_id:
|
||||
new_msg_id = self._last_msg_id + 4
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ class MtProtoSender:
|
|||
"""MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)"""
|
||||
|
||||
def __init__(self, transport, session):
|
||||
self._transport = transport
|
||||
self.transport = transport
|
||||
self.session = session
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -33,11 +33,14 @@ class MtProtoSender:
|
|||
|
||||
def connect(self):
|
||||
"""Connects to the server"""
|
||||
self._transport.connect()
|
||||
self.transport.connect()
|
||||
|
||||
def is_connected(self):
|
||||
return self.transport.is_connected()
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnects from the server"""
|
||||
self._transport.close()
|
||||
self.transport.close()
|
||||
|
||||
# region Send and receive
|
||||
|
||||
|
@ -73,11 +76,12 @@ class MtProtoSender:
|
|||
|
||||
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"
|
||||
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
|
||||
the 'updates' list (which cannot be None).
|
||||
|
@ -96,8 +100,8 @@ class MtProtoSender:
|
|||
# or, if there is no request, until we read an update
|
||||
while (request and not request.confirm_received) or \
|
||||
(not request and not updates):
|
||||
self._logger.info('Trying to .receive() the request result...')
|
||||
seq, body = self._transport.receive(timeout)
|
||||
self._logger.debug('Trying to .receive() the request result...')
|
||||
seq, body = self.transport.receive(**kwargs)
|
||||
message, remote_msg_id, remote_seq = self._decode_msg(body)
|
||||
|
||||
with BinaryReader(message) as reader:
|
||||
|
@ -110,21 +114,19 @@ class MtProtoSender:
|
|||
self._pending_receive.remove(request)
|
||||
except ValueError: pass
|
||||
|
||||
self._logger.info('Request result received')
|
||||
self._logger.debug('Request result received')
|
||||
self._logger.debug('receive() released the lock')
|
||||
|
||||
def receive_updates(self, timeout=timedelta(seconds=5)):
|
||||
"""Receives one or more update objects
|
||||
and returns them as a list
|
||||
"""
|
||||
def receive_updates(self, **kwargs):
|
||||
"""Wrapper for .receive(request=None, updates=[])"""
|
||||
updates = []
|
||||
self.receive(timeout=timeout, updates=updates)
|
||||
self.receive(updates=updates, **kwargs)
|
||||
return updates
|
||||
|
||||
def cancel_receive(self):
|
||||
"""Cancels any pending receive operation
|
||||
by raising a ReadCancelledError"""
|
||||
self._transport.cancel_receive()
|
||||
self.transport.cancel_receive()
|
||||
|
||||
# endregion
|
||||
|
||||
|
@ -141,7 +143,7 @@ class MtProtoSender:
|
|||
plain_writer.write_long(self.session.id, signed=False)
|
||||
plain_writer.write_long(request.request_msg_id)
|
||||
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(packet)
|
||||
|
@ -157,7 +159,7 @@ class MtProtoSender:
|
|||
self.session.auth_key.key_id, signed=False)
|
||||
cipher_writer.write(msg_key)
|
||||
cipher_writer.write(cipher_text)
|
||||
self._transport.send(cipher_writer.get_bytes())
|
||||
self.transport.send(cipher_writer.get_bytes())
|
||||
|
||||
def _decode_msg(self, body):
|
||||
"""Decodes an received encrypted message body bytes"""
|
||||
|
@ -224,10 +226,10 @@ class MtProtoSender:
|
|||
ack = reader.tgread_object()
|
||||
for r in self._pending_receive:
|
||||
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:
|
||||
self._logger.info('Message ack confirmed a request')
|
||||
self._logger.debug('Message ack confirmed a request')
|
||||
r.confirm_received = True
|
||||
|
||||
return True
|
||||
|
@ -245,7 +247,7 @@ class MtProtoSender:
|
|||
|
||||
return True
|
||||
|
||||
self._logger.warning('Unknown message: {}'.format(hex(code)))
|
||||
self._logger.debug('Unknown message: {}'.format(hex(code)))
|
||||
return False
|
||||
|
||||
# endregion
|
||||
|
@ -255,13 +257,13 @@ class MtProtoSender:
|
|||
def _handle_pong(self, msg_id, sequence, reader):
|
||||
self._logger.debug('Handling pong')
|
||||
reader.read_int(signed=False) # code
|
||||
received_msg_id = reader.read_long(signed=False)
|
||||
received_msg_id = reader.read_long()
|
||||
|
||||
try:
|
||||
request = next(r for r in self._pending_receive
|
||||
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
|
||||
except StopIteration: pass
|
||||
|
||||
|
@ -272,23 +274,28 @@ class MtProtoSender:
|
|||
reader.read_int(signed=False) # code
|
||||
size = reader.read_int()
|
||||
for _ in range(size):
|
||||
inner_msg_id = reader.read_long(signed=False)
|
||||
inner_msg_id = reader.read_long()
|
||||
reader.read_int() # inner_sequence
|
||||
inner_length = reader.read_int()
|
||||
begin_position = reader.tell_position()
|
||||
|
||||
# Note that this code is IMPORTANT for skipping RPC results of
|
||||
# lost requests (i.e., ones from the previous connection session)
|
||||
try:
|
||||
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)
|
||||
raise
|
||||
|
||||
return True
|
||||
|
||||
def _handle_bad_server_salt(self, msg_id, sequence, reader):
|
||||
self._logger.debug('Handling bad server salt')
|
||||
reader.read_int(signed=False) # code
|
||||
bad_msg_id = reader.read_long(signed=False)
|
||||
bad_msg_id = reader.read_long()
|
||||
reader.read_int() # bad_msg_seq_no
|
||||
reader.read_int() # error_code
|
||||
new_salt = reader.read_long(signed=False)
|
||||
|
@ -306,7 +313,7 @@ class MtProtoSender:
|
|||
def _handle_bad_msg_notification(self, msg_id, sequence, reader):
|
||||
self._logger.debug('Handling bad message notification')
|
||||
reader.read_int(signed=False) # code
|
||||
reader.read_long(signed=False) # request_id
|
||||
reader.read_long() # request_id
|
||||
reader.read_int() # request_sequence
|
||||
|
||||
error_code = reader.read_int()
|
||||
|
@ -316,8 +323,8 @@ class MtProtoSender:
|
|||
# Use the current msg_id to determine the right time offset.
|
||||
self.session.update_time_offset(correct_msg_id=msg_id)
|
||||
self.session.save()
|
||||
self._logger.warning('Read Bad Message error: ' + str(error))
|
||||
self._logger.info('Attempting to use the correct time offset.')
|
||||
self._logger.debug('Read Bad Message error: ' + str(error))
|
||||
self._logger.debug('Attempting to use the correct time offset.')
|
||||
return True
|
||||
else:
|
||||
raise error
|
||||
|
@ -325,7 +332,7 @@ class MtProtoSender:
|
|||
def _handle_rpc_result(self, msg_id, sequence, reader):
|
||||
self._logger.debug('Handling RPC result')
|
||||
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)
|
||||
|
||||
try:
|
||||
|
@ -344,7 +351,7 @@ class MtProtoSender:
|
|||
self._need_confirmation.append(request_id)
|
||||
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):
|
||||
# Must resend this request, if any
|
||||
if request:
|
||||
|
@ -366,7 +373,7 @@ class MtProtoSender:
|
|||
else:
|
||||
# If it's really a result for RPC from previous connection
|
||||
# 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
|
||||
|
||||
def _handle_gzip_packed(self, msg_id, sequence, reader, updates):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from binascii import crc32
|
||||
from zlib import crc32
|
||||
from datetime import timedelta
|
||||
|
||||
from ..errors import InvalidChecksumError
|
||||
|
@ -7,19 +7,26 @@ from ..extensions import BinaryWriter
|
|||
|
||||
|
||||
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.port = port
|
||||
self.tcp_client = TcpClient(proxy)
|
||||
self.timeout = timeout
|
||||
self.send_counter = 0
|
||||
|
||||
def connect(self):
|
||||
"""Connects to the specified IP address and port"""
|
||||
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
|
||||
# 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):
|
||||
"""Sends the given packet (bytes array) to the connected peer"""
|
||||
if not self.tcp_client.connected:
|
||||
|
@ -36,10 +43,14 @@ class TcpTransport:
|
|||
self.send_counter += 1
|
||||
self.tcp_client.write(writer.get_bytes())
|
||||
|
||||
def receive(self, timeout=timedelta(seconds=5)):
|
||||
"""Receives a TCP message (tuple(sequence number, body)) from the connected peer.
|
||||
There is a default timeout of 5 seconds before the operation is cancelled.
|
||||
Timeout can be set to None for no timeout"""
|
||||
def receive(self, **kwargs):
|
||||
"""Receives a TCP message (tuple(sequence number, body)) from the
|
||||
connected peer.
|
||||
|
||||
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
|
||||
packet_length_bytes = self.tcp_client.read(4, timeout)
|
||||
|
|
|
@ -5,22 +5,27 @@ from os import path
|
|||
|
||||
# Import some externalized utilities to work with the Telegram types and more
|
||||
from . import helpers as utils
|
||||
from .errors import RPCError, FloodWaitError
|
||||
from .errors import (
|
||||
RPCError, FloodWaitError, FileMigrateError, TypeNotFoundError
|
||||
)
|
||||
from .network import authenticator, MtProtoSender, TcpTransport
|
||||
from .utils import get_appropriated_part_size
|
||||
|
||||
# For sending and receiving requests
|
||||
from .tl import MTProtoRequest
|
||||
from .tl import TLObject, JsonSession
|
||||
from .tl.all_tlobjects import layer
|
||||
from .tl.functions import (InitConnectionRequest, InvokeWithLayerRequest)
|
||||
|
||||
# Initial request
|
||||
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
|
||||
from .tl.functions.upload import (
|
||||
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest)
|
||||
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest
|
||||
)
|
||||
|
||||
# All the types we need to work with
|
||||
from .tl.types import InputFile, InputFileBig
|
||||
|
@ -47,11 +52,12 @@ class TelegramBareClient:
|
|||
"""
|
||||
|
||||
# Current TelegramClient version
|
||||
__version__ = '0.11'
|
||||
__version__ = '0.11.5'
|
||||
|
||||
# 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.
|
||||
Session must always be a Session instance, and an optional proxy
|
||||
can also be specified to be used on the connection.
|
||||
|
@ -60,11 +66,17 @@ class TelegramBareClient:
|
|||
self.api_id = int(api_id)
|
||||
self.api_hash = api_hash
|
||||
self.proxy = proxy
|
||||
self._timeout = timeout
|
||||
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
|
||||
self.dc_options = None
|
||||
self.sender = None
|
||||
self._sender = None
|
||||
|
||||
# endregion
|
||||
|
||||
|
@ -79,8 +91,16 @@ class TelegramBareClient:
|
|||
If 'exported_auth' is not None, it will be used instead to
|
||||
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,
|
||||
self.session.port, proxy=self.proxy)
|
||||
self.session.port,
|
||||
proxy=self.proxy,
|
||||
timeout=self._timeout)
|
||||
|
||||
try:
|
||||
if not self.session.auth_key:
|
||||
|
@ -89,8 +109,8 @@ class TelegramBareClient:
|
|||
|
||||
self.session.save()
|
||||
|
||||
self.sender = MtProtoSender(transport, self.session)
|
||||
self.sender.connect()
|
||||
self._sender = MtProtoSender(transport, self.session)
|
||||
self._sender.connect()
|
||||
|
||||
# Now it's time to send an InitConnectionRequest
|
||||
# This must always be invoked with the layer we'll be using
|
||||
|
@ -106,34 +126,40 @@ class TelegramBareClient:
|
|||
system_version=self.session.system_version,
|
||||
app_version=self.session.app_version,
|
||||
lang_code=self.session.lang_code,
|
||||
system_lang_code=self.session.system_lang_code,
|
||||
lang_pack='', # "langPacks are for official apps only"
|
||||
query=query)
|
||||
|
||||
result = self.invoke(
|
||||
InvokeWithLayerRequest(
|
||||
layer=layer, query=request))
|
||||
result = self(InvokeWithLayerRequest(
|
||||
layer=layer, query=request
|
||||
))
|
||||
|
||||
if exported_auth is not None:
|
||||
# TODO Don't actually need this for exported authorizations,
|
||||
# they're only valid on such data center.
|
||||
result = self.invoke(GetConfigRequest())
|
||||
result = self(GetConfigRequest())
|
||||
|
||||
# We're only interested in the DC options,
|
||||
# although many other options are available!
|
||||
self.dc_options = result.dc_options
|
||||
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:
|
||||
# Probably errors from the previous session, ignore them
|
||||
self.disconnect()
|
||||
self._logger.warning('Could not stabilise initial connection: {}'
|
||||
self._logger.debug('Could not stabilise initial connection: {}'
|
||||
.format(error))
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnects from the Telegram server"""
|
||||
if self.sender:
|
||||
self.sender.disconnect()
|
||||
self.sender = None
|
||||
if self._sender:
|
||||
self._sender.disconnect()
|
||||
self._sender = None
|
||||
|
||||
def reconnect(self, new_dc=None):
|
||||
"""Disconnects and connects again (effectively reconnecting).
|
||||
|
@ -154,6 +180,30 @@ class TelegramBareClient:
|
|||
|
||||
# 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
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
# 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.
|
||||
|
||||
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
|
||||
in such list. Otherwise, update objects will be ignored.
|
||||
"""
|
||||
if not isinstance(request, MTProtoRequest):
|
||||
raise ValueError('You can only invoke MtProtoRequests')
|
||||
if not isinstance(request, TLObject) and not request.content_related:
|
||||
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!')
|
||||
|
||||
try:
|
||||
self.sender.send(request)
|
||||
self.sender.receive(request, timeout, updates=updates)
|
||||
self._sender.send(request)
|
||||
self._sender.receive(request, updates=updates)
|
||||
return request.result
|
||||
|
||||
except ConnectionResetError:
|
||||
self._logger.info('Server disconnected us. Reconnecting and '
|
||||
self._logger.debug('Server disconnected us. Reconnecting and '
|
||||
'resending request...')
|
||||
self.reconnect()
|
||||
return self.invoke(request, timeout=timeout)
|
||||
return self.invoke(request)
|
||||
|
||||
except FloodWaitError:
|
||||
self.disconnect()
|
||||
raise
|
||||
|
||||
# Let people use client(SomeRequest()) instead client.invoke(...)
|
||||
__call__ = invoke
|
||||
|
||||
# endregion
|
||||
|
||||
# region Uploading media
|
||||
|
@ -250,7 +348,7 @@ class TelegramBareClient:
|
|||
else:
|
||||
request = SaveFilePartRequest(file_id, part_index, part)
|
||||
|
||||
result = self.invoke(request)
|
||||
result = self(request)
|
||||
if result:
|
||||
if not is_large:
|
||||
# No need to update the hash if it's a large file
|
||||
|
@ -305,12 +403,21 @@ class TelegramBareClient:
|
|||
else:
|
||||
f = file
|
||||
|
||||
# The used client will change if FileMigrateError occurs
|
||||
client = self
|
||||
|
||||
try:
|
||||
offset_index = 0
|
||||
while True:
|
||||
offset = offset_index * part_size
|
||||
result = self.invoke(
|
||||
|
||||
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
|
||||
|
||||
# If we have received no data (0 bytes), the file is over
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
from datetime import timedelta
|
||||
from mimetypes import guess_type
|
||||
from threading import Event, RLock, Thread
|
||||
from time import sleep
|
||||
from time import sleep, time
|
||||
|
||||
from . import TelegramBareClient
|
||||
|
||||
# Import some externalized utilities to work with the Telegram types and more
|
||||
from . import helpers as utils
|
||||
from .errors import (RPCError, UnauthorizedError, InvalidParameterError,
|
||||
ReadCancelledError, FileMigrateError, PhoneMigrateError,
|
||||
NetworkMigrateError, UserMigrateError, PhoneCodeEmptyError,
|
||||
ReadCancelledError, PhoneCodeEmptyError,
|
||||
PhoneMigrateError, NetworkMigrateError, UserMigrateError,
|
||||
PhoneCodeExpiredError, PhoneCodeHashEmptyError,
|
||||
PhoneCodeInvalidError, InvalidChecksumError)
|
||||
|
||||
# For sending and receiving requests
|
||||
from .tl import MTProtoRequest, Session, JsonSession
|
||||
from .tl import Session, JsonSession
|
||||
|
||||
# Required to get the password salt
|
||||
from .tl.functions.account import GetPasswordRequest
|
||||
|
@ -24,9 +24,6 @@ from .tl.functions.auth import (CheckPasswordRequest, LogOutRequest,
|
|||
SendCodeRequest, SignInRequest,
|
||||
SignUpRequest, ImportBotAuthorizationRequest)
|
||||
|
||||
# Required to work with different data centers
|
||||
from .tl.functions.auth import ExportAuthorizationRequest
|
||||
|
||||
# Easier access to common methods
|
||||
from .tl.functions.messages import (
|
||||
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
|
||||
|
@ -35,6 +32,9 @@ from .tl.functions.messages import (
|
|||
# For .get_me() and ensuring we're authorized
|
||||
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
|
||||
from .tl.types import (
|
||||
ChatPhotoEmpty, DocumentAttributeAudio, DocumentAttributeFilename,
|
||||
|
@ -61,7 +61,9 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
def __init__(self, session, api_id, api_hash, proxy=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.
|
||||
|
||||
Session can either be a `str` object (filename for the .session)
|
||||
|
@ -74,8 +76,8 @@ class TelegramClient(TelegramBareClient):
|
|||
system_version = platform.system()
|
||||
app_version = TelegramClient.__version__
|
||||
lang_code = 'en'
|
||||
system_lang_code = lang_code
|
||||
"""
|
||||
|
||||
if not api_id or not api_hash:
|
||||
raise PermissionError(
|
||||
"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)
|
||||
if isinstance(session, str) or session is None:
|
||||
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(
|
||||
'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)
|
||||
self._lock = RLock()
|
||||
|
||||
# Methods to be called when an update is received
|
||||
# Updates-related members
|
||||
self._update_handlers = []
|
||||
self._updates_thread_running = 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
|
||||
if device_model:
|
||||
self.session.device_model = device_model
|
||||
|
@ -112,10 +117,9 @@ class TelegramClient(TelegramBareClient):
|
|||
if lang_code:
|
||||
self.session.lang_code = lang_code
|
||||
|
||||
# Cache "exported" senders 'dc_id: MtProtoSender' and
|
||||
# their corresponding sessions not to recreate them all
|
||||
# the time since it's a (somewhat expensive) process.
|
||||
self._cached_clients = {}
|
||||
self.session.system_lang_code = \
|
||||
system_lang_code if system_lang_code else self.session.lang_code
|
||||
|
||||
self._updates_thread = None
|
||||
self._phone_code_hashes = {}
|
||||
|
||||
|
@ -129,15 +133,17 @@ class TelegramClient(TelegramBareClient):
|
|||
not the same as authenticating the desired user itself, which
|
||||
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.
|
||||
"""
|
||||
return super(TelegramClient, self).connect()
|
||||
return super().connect()
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnects from the Telegram server
|
||||
and stops all the spawned threads"""
|
||||
self._set_updates_thread(running=False)
|
||||
super(TelegramClient, self).disconnect()
|
||||
super().disconnect()
|
||||
|
||||
# Also disconnect all the cached senders
|
||||
for sender in self._cached_clients.values():
|
||||
|
@ -149,52 +155,6 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
# 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):
|
||||
"""Creates a new connection which can be used in parallel
|
||||
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
|
||||
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:
|
||||
client = TelegramBareClient(self.session, self.api_id, self.api_hash,
|
||||
proxy=self.proxy)
|
||||
client = TelegramBareClient(
|
||||
self.session, self.api_id, self.api_hash, proxy=self.proxy)
|
||||
client.connect()
|
||||
else:
|
||||
client = self._get_exported_client(on_dc, bypass_cache=True)
|
||||
|
@ -225,7 +180,7 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
# 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.
|
||||
|
||||
An optional timeout can be specified to cancel the operation if no
|
||||
|
@ -233,21 +188,16 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
*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():
|
||||
self.sender.cancel_receive()
|
||||
self._sender.cancel_receive()
|
||||
|
||||
try:
|
||||
self._lock.acquire()
|
||||
|
||||
updates = [] if self._update_handlers else None
|
||||
result = super(TelegramClient, self).invoke(
|
||||
request, timeout=timeout, updates=updates)
|
||||
result = super().invoke(
|
||||
request, updates=updates
|
||||
)
|
||||
|
||||
if updates:
|
||||
for update in updates:
|
||||
|
@ -258,18 +208,20 @@ class TelegramClient(TelegramBareClient):
|
|||
return result
|
||||
|
||||
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 {}'
|
||||
.format(e.new_dc))
|
||||
|
||||
self.reconnect(new_dc=e.new_dc)
|
||||
return self.invoke(request, timeout=timeout)
|
||||
return self.invoke(request)
|
||||
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def invoke_on_dc(self, request, dc_id,
|
||||
timeout=timedelta(seconds=5), reconnect=False):
|
||||
# Let people use client(SomeRequest()) instead client.invoke(...)
|
||||
__call__ = invoke
|
||||
|
||||
def invoke_on_dc(self, request, dc_id, reconnect=False):
|
||||
"""Invokes the given request on a different DC
|
||||
by making use of the exported MtProtoSenders.
|
||||
|
||||
|
@ -286,8 +238,7 @@ class TelegramClient(TelegramBareClient):
|
|||
if reconnect:
|
||||
raise
|
||||
else:
|
||||
return self.invoke_on_dc(request, dc_id,
|
||||
timeout=timeout, reconnect=True)
|
||||
return self.invoke_on_dc(request, dc_id, reconnect=True)
|
||||
|
||||
# region Authorization requests
|
||||
|
||||
|
@ -298,7 +249,7 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
def send_code_request(self, 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))
|
||||
|
||||
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.')
|
||||
|
||||
try:
|
||||
result = self.invoke(SignInRequest(
|
||||
result = self(SignInRequest(
|
||||
phone_number, self._phone_code_hashes[phone_number], code))
|
||||
|
||||
except (PhoneCodeEmptyError, PhoneCodeExpiredError,
|
||||
|
@ -332,12 +283,12 @@ class TelegramClient(TelegramBareClient):
|
|||
return None
|
||||
|
||||
elif password:
|
||||
salt = self.invoke(GetPasswordRequest()).current_salt
|
||||
result = self.invoke(
|
||||
salt = self(GetPasswordRequest()).current_salt
|
||||
result = self(
|
||||
CheckPasswordRequest(utils.get_password_hash(password, salt)))
|
||||
|
||||
elif bot_token:
|
||||
result = self.invoke(ImportBotAuthorizationRequest(
|
||||
result = self(ImportBotAuthorizationRequest(
|
||||
flags=0, bot_auth_token=bot_token,
|
||||
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=''):
|
||||
"""Signs up to Telegram. Make sure you sent a code request first!"""
|
||||
result = self.invoke(
|
||||
result = self(
|
||||
SignUpRequest(
|
||||
phone_number=phone_number,
|
||||
phone_code_hash=self._phone_code_hashes[phone_number],
|
||||
|
@ -366,9 +317,9 @@ class TelegramClient(TelegramBareClient):
|
|||
Returns True if everything went okay."""
|
||||
|
||||
# Special flag when logging out (so the ack request confirms it)
|
||||
self.sender.logging_out = True
|
||||
self._sender.logging_out = True
|
||||
try:
|
||||
self.invoke(LogOutRequest())
|
||||
self(LogOutRequest())
|
||||
self.disconnect()
|
||||
if not self.session.delete():
|
||||
return False
|
||||
|
@ -377,14 +328,14 @@ class TelegramClient(TelegramBareClient):
|
|||
return True
|
||||
except (RPCError, ConnectionError):
|
||||
# Something happened when logging out, restore the state back
|
||||
self.sender.logging_out = False
|
||||
self._sender.logging_out = False
|
||||
return False
|
||||
|
||||
def get_me(self):
|
||||
"""Gets "me" (the self user) which is currently authenticated,
|
||||
or None if the request fails (hence, not authenticated)."""
|
||||
try:
|
||||
return self.invoke(GetUsersRequest([InputUserSelf()]))[0]
|
||||
return self(GetUsersRequest([InputUserSelf()]))[0]
|
||||
except UnauthorizedError:
|
||||
return None
|
||||
|
||||
|
@ -405,7 +356,7 @@ class TelegramClient(TelegramBareClient):
|
|||
corresponding to that dialog.
|
||||
"""
|
||||
|
||||
r = self.invoke(
|
||||
r = self(
|
||||
GetDialogsRequest(
|
||||
offset_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
|
@ -422,16 +373,18 @@ class TelegramClient(TelegramBareClient):
|
|||
def send_message(self,
|
||||
entity,
|
||||
message,
|
||||
no_web_page=False):
|
||||
link_preview=True):
|
||||
"""Sends a message to the given entity (or input peer)
|
||||
and returns the sent message ID"""
|
||||
request = SendMessageRequest(
|
||||
peer=get_input_peer(entity),
|
||||
message=message,
|
||||
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
|
||||
|
||||
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]).
|
||||
Note that the sender can be null if it was not found!
|
||||
"""
|
||||
result = self.invoke(
|
||||
GetHistoryRequest(
|
||||
result = self(GetHistoryRequest(
|
||||
get_input_peer(entity),
|
||||
limit=limit,
|
||||
offset_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
max_id=max_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)
|
||||
# or simply a messages TLObject. In the later case, no "count"
|
||||
|
@ -498,7 +451,10 @@ class TelegramClient(TelegramBareClient):
|
|||
else:
|
||||
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
|
||||
|
||||
|
@ -537,7 +493,7 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
def send_media_file(self, input_media, entity):
|
||||
"""Sends any input_media (contact, document, photo...) to the given entity"""
|
||||
self.invoke(SendMediaRequest(
|
||||
self(SendMediaRequest(
|
||||
peer=get_input_peer(entity),
|
||||
media=input_media
|
||||
))
|
||||
|
@ -580,36 +536,41 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
def download_msg_media(self,
|
||||
message_media,
|
||||
file_path,
|
||||
file,
|
||||
add_extension=True,
|
||||
progress_callback=None):
|
||||
"""Downloads the given MessageMedia (Photo, Document or Contact)
|
||||
into the desired file_path, 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).
|
||||
This will be called every time a part is downloaded"""
|
||||
into the desired file (a stream or str), optionally finding its
|
||||
extension automatically.
|
||||
|
||||
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:
|
||||
return self.download_photo(message_media, file_path, add_extension,
|
||||
return self.download_photo(message_media, file, add_extension,
|
||||
progress_callback)
|
||||
|
||||
elif type(message_media) == MessageMediaDocument:
|
||||
return self.download_document(message_media, file_path,
|
||||
return self.download_document(message_media, file,
|
||||
add_extension, progress_callback)
|
||||
|
||||
elif type(message_media) == MessageMediaContact:
|
||||
return self.download_contact(message_media, file_path,
|
||||
return self.download_contact(message_media, file,
|
||||
add_extension)
|
||||
|
||||
def download_photo(self,
|
||||
message_media_photo,
|
||||
file_path,
|
||||
file,
|
||||
add_extension=False,
|
||||
progress_callback=None):
|
||||
"""Downloads MessageMediaPhoto's largest size into the desired
|
||||
file_path, 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).
|
||||
This will be called every time a part is downloaded"""
|
||||
"""Downloads MessageMediaPhoto's largest size into the desired file
|
||||
(a stream or str), optionally finding its extension automatically.
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
# Determine the photo and its largest size
|
||||
photo = message_media_photo.photo
|
||||
|
@ -617,8 +578,8 @@ class TelegramClient(TelegramBareClient):
|
|||
file_size = largest_size.size
|
||||
largest_size = largest_size.location
|
||||
|
||||
if add_extension:
|
||||
file_path += get_extension(message_media_photo)
|
||||
if isinstance(file, str) and add_extension:
|
||||
file += get_extension(message_media_photo)
|
||||
|
||||
# Download the media with the largest size input file location
|
||||
self.download_file(
|
||||
|
@ -627,42 +588,45 @@ class TelegramClient(TelegramBareClient):
|
|||
local_id=largest_size.local_id,
|
||||
secret=largest_size.secret
|
||||
),
|
||||
file_path,
|
||||
file,
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback
|
||||
)
|
||||
return file_path
|
||||
return file
|
||||
|
||||
def download_document(self,
|
||||
message_media_document,
|
||||
file_path=None,
|
||||
file=None,
|
||||
add_extension=True,
|
||||
progress_callback=None):
|
||||
"""Downloads the given MessageMediaDocument into the desired
|
||||
file_path, 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,
|
||||
uploaded size (in bytes) and total file size (in bytes).
|
||||
This will be called every time a part is downloaded"""
|
||||
"""Downloads the given MessageMediaDocument into the desired file
|
||||
(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, uploaded size and total file size (both in bytes).
|
||||
This will be called every time a part is downloaded
|
||||
"""
|
||||
document = message_media_document.document
|
||||
file_size = document.size
|
||||
|
||||
# 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:
|
||||
if type(attr) == DocumentAttributeFilename:
|
||||
file_path = attr.file_name
|
||||
file = attr.file_name
|
||||
break # This attribute has higher preference
|
||||
|
||||
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'
|
||||
'. Please provide a valid file_path manually')
|
||||
|
||||
if add_extension:
|
||||
file_path += get_extension(message_media_document)
|
||||
if isinstance(file, str) and add_extension:
|
||||
file += get_extension(message_media_document)
|
||||
|
||||
self.download_file(
|
||||
InputDocumentFileLocation(
|
||||
|
@ -670,74 +634,48 @@ class TelegramClient(TelegramBareClient):
|
|||
access_hash=document.access_hash,
|
||||
version=document.version
|
||||
),
|
||||
file_path,
|
||||
file,
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback
|
||||
)
|
||||
return file_path
|
||||
return file
|
||||
|
||||
@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"""
|
||||
|
||||
first_name = message_media_contact.first_name
|
||||
last_name = message_media_contact.last_name
|
||||
phone_number = message_media_contact.phone_number
|
||||
|
||||
if isinstance(file, str):
|
||||
# The only way we can save a contact in an understandable
|
||||
# way by phones is by using the .vCard format
|
||||
if add_extension:
|
||||
file_path += '.vcard'
|
||||
file += '.vcard'
|
||||
|
||||
# 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:
|
||||
file.write('BEGIN:VCARD\n')
|
||||
file.write('VERSION:4.0\n')
|
||||
file.write('N:{};{};;;\n'.format(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
|
||||
f.write('BEGIN:VCARD\n')
|
||||
f.write('VERSION:4.0\n')
|
||||
f.write('N:{};{};;;\n'.format(
|
||||
first_name, last_name if last_name else '')
|
||||
)
|
||||
except FileMigrateError as e:
|
||||
on_dc = e.new_dc
|
||||
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()
|
||||
|
||||
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
|
||||
)
|
||||
return file
|
||||
|
||||
# endregion
|
||||
|
||||
|
@ -748,7 +686,7 @@ class TelegramClient(TelegramBareClient):
|
|||
def add_update_handler(self, handler):
|
||||
"""Adds an update handler (a function which takes a TLObject,
|
||||
an update, as its parameter) and listens for updates"""
|
||||
if not self.sender:
|
||||
if not self._sender:
|
||||
raise RuntimeError("You can't add update handlers until you've "
|
||||
"successfully connected to the server.")
|
||||
|
||||
|
@ -771,7 +709,7 @@ class TelegramClient(TelegramBareClient):
|
|||
return
|
||||
|
||||
# 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:
|
||||
self._updates_thread_running.set()
|
||||
if not self._updates_thread:
|
||||
|
@ -783,7 +721,7 @@ class TelegramClient(TelegramBareClient):
|
|||
else:
|
||||
self._updates_thread_running.clear()
|
||||
if self._updates_thread_receiving.is_set():
|
||||
self.sender.cancel_receive()
|
||||
self._sender.cancel_receive()
|
||||
|
||||
def _updates_thread_method(self):
|
||||
"""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'
|
||||
)
|
||||
|
||||
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._logger.info(
|
||||
self._logger.debug(
|
||||
'Received {} update(s) from the updates thread'
|
||||
.format(len(updates))
|
||||
)
|
||||
|
@ -817,28 +759,28 @@ class TelegramClient(TelegramBareClient):
|
|||
handler(update)
|
||||
|
||||
except ConnectionResetError:
|
||||
self._logger.info('Server disconnected us. Reconnecting...')
|
||||
self._logger.debug('Server disconnected us. Reconnecting...')
|
||||
self.reconnect()
|
||||
|
||||
except TimeoutError:
|
||||
self._logger.debug('Receiving updates timed out')
|
||||
|
||||
except ReadCancelledError:
|
||||
self._logger.info('Receiving updates cancelled')
|
||||
self._logger.debug('Receiving updates cancelled')
|
||||
|
||||
except BrokenPipeError:
|
||||
self._logger.info('Tcp session is broken. Reconnecting...')
|
||||
self._logger.debug('Tcp session is broken. Reconnecting...')
|
||||
self.reconnect()
|
||||
|
||||
except InvalidChecksumError:
|
||||
self._logger.info('MTProto session is broken. Reconnecting...')
|
||||
self._logger.debug('MTProto session is broken. Reconnecting...')
|
||||
self.reconnect()
|
||||
|
||||
except OSError:
|
||||
self._logger.warning('OSError on updates thread, %s logging out',
|
||||
'was' if self.sender.logging_out else 'was not')
|
||||
self._logger.debug('OSError on updates thread, %s logging out',
|
||||
'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
|
||||
# TODO Not sure why this happens because we call disconnect()...
|
||||
self._set_updates_thread(running=False)
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
from .mtproto_request import MTProtoRequest
|
||||
from .tlobject import TLObject
|
||||
from .session import Session, JsonSession
|
||||
|
|
|
@ -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
|
|
@ -2,7 +2,6 @@ import json
|
|||
import os
|
||||
import pickle
|
||||
import platform
|
||||
import random
|
||||
import time
|
||||
from threading import Lock
|
||||
from base64 import b64encode, b64decode
|
||||
|
@ -65,15 +64,10 @@ class Session:
|
|||
return self.sequence * 2
|
||||
|
||||
def get_new_msg_id(self):
|
||||
"""Generates a new message ID based on the current time (in ms) since epoch"""
|
||||
# Refer to mtproto_plain_sender.py for the original method, this is a simple copy
|
||||
ms_time = int(time.time() * 1000)
|
||||
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32)
|
||||
| # "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"
|
||||
now = time.time()
|
||||
nanoseconds = int((now - int(now)) * 1e+9)
|
||||
# "message identifiers are divisible by 4"
|
||||
new_msg_id = (int(now) << 32) | (nanoseconds << 2)
|
||||
|
||||
if self.last_message_id >= new_msg_id:
|
||||
new_msg_id = self.last_message_id + 4
|
||||
|
@ -113,14 +107,19 @@ class JsonSession:
|
|||
self.system_version = session.system_version
|
||||
self.app_version = session.app_version
|
||||
self.lang_code = session.lang_code
|
||||
self.system_lang_code = session.system_lang_code
|
||||
self.lang_pack = session.lang_pack
|
||||
|
||||
else: # str / None
|
||||
self.session_user_id = session_user_id
|
||||
|
||||
self.device_model = platform.node()
|
||||
self.system_version = platform.system()
|
||||
self.app_version = '1.0' # note: '0' will provoke error
|
||||
system = platform.uname()
|
||||
self.device_model = system.system if system.system else 'Unknown'
|
||||
self.system_version = system.release if system.release else '1.0'
|
||||
self.app_version = '1.0' # '0' will provoke error
|
||||
self.lang_code = 'en'
|
||||
self.system_lang_code = self.lang_code
|
||||
self.lang_pack = ''
|
||||
|
||||
# Cross-thread safety
|
||||
self._lock = Lock()
|
||||
|
@ -133,7 +132,7 @@ class JsonSession:
|
|||
self._sequence = 0
|
||||
self.salt = 0 # Unsigned long
|
||||
self.time_offset = 0
|
||||
self.last_message_id = 0 # Long
|
||||
self._last_msg_id = 0 # Long
|
||||
|
||||
def save(self):
|
||||
"""Saves the current session object as session_user_id.session"""
|
||||
|
@ -229,19 +228,18 @@ class JsonSession:
|
|||
def get_new_msg_id(self):
|
||||
"""Generates a new unique message ID based on the current
|
||||
time (in ms) since epoch"""
|
||||
# Refer to mtproto_plain_sender.py for the original method,
|
||||
ms_time = int(time.time() * 1000)
|
||||
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32)
|
||||
| # "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"
|
||||
# Refer to mtproto_plain_sender.py for the original method
|
||||
now = time.time()
|
||||
nanoseconds = int((now - int(now)) * 1e+9)
|
||||
# "message identifiers are divisible by 4"
|
||||
new_msg_id = (int(now) << 32) | (nanoseconds << 2)
|
||||
|
||||
if self.last_message_id >= new_msg_id:
|
||||
new_msg_id = self.last_message_id + 4
|
||||
with self._lock:
|
||||
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
|
||||
|
||||
def update_time_offset(self, correct_msg_id):
|
||||
|
|
113
telethon/tl/tlobject.py
Normal file
113
telethon/tl/tlobject.py
Normal 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
|
|
@ -7,7 +7,8 @@ from mimetypes import add_type, guess_extension
|
|||
from .tl.types import (
|
||||
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
||||
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
|
||||
InputPeerSelf, MessageMediaDocument, MessageMediaPhoto, PeerChannel,
|
||||
MessageMediaDocument, MessageMediaPhoto, PeerChannel, InputChannel,
|
||||
UserEmpty, InputUser, InputUserEmpty, InputUserSelf, InputPeerSelf,
|
||||
PeerChat, PeerUser, User, UserFull, UserProfilePhoto)
|
||||
|
||||
|
||||
|
@ -52,10 +53,13 @@ def get_extension(media):
|
|||
def get_input_peer(entity):
|
||||
"""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."""
|
||||
if type(entity).subclass_of_id == 0xc91c90b6: # crc32('InputUser')
|
||||
if type(entity).subclass_of_id == 0xc91c90b6: # crc32(b'InputPeer')
|
||||
return entity
|
||||
|
||||
if isinstance(entity, User):
|
||||
if entity.is_self:
|
||||
return InputPeerSelf()
|
||||
else:
|
||||
return InputPeerUser(entity.id, entity.access_hash)
|
||||
|
||||
if any(isinstance(entity, c) for c in (
|
||||
|
@ -67,16 +71,64 @@ def get_input_peer(entity):
|
|||
return InputPeerChannel(entity.id, entity.access_hash)
|
||||
|
||||
# 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):
|
||||
return InputPeerUser(entity.user.id, entity.user.access_hash)
|
||||
return get_input_peer(entity.user)
|
||||
|
||||
if isinstance(entity, ChatFull):
|
||||
return InputPeerChat(entity.id)
|
||||
|
||||
if isinstance(entity, PeerChat):
|
||||
return InputPeerChat(entity.chat_id)
|
||||
|
||||
raise ValueError('Cannot cast {} to any kind of InputPeer.'
|
||||
.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):
|
||||
"""Finds the corresponding user or chat given a peer.
|
||||
Returns None if it was not found"""
|
||||
|
|
|
@ -217,7 +217,7 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
# Send chat message (if any)
|
||||
elif msg:
|
||||
self.send_message(
|
||||
entity, msg, no_web_page=True)
|
||||
entity, msg, link_preview=False)
|
||||
|
||||
def send_photo(self, path, entity):
|
||||
print('Uploading {}...'.format(path))
|
||||
|
@ -250,7 +250,7 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
print('Downloading media with name {}...'.format(output))
|
||||
output = self.download_msg_media(
|
||||
msg.media,
|
||||
file_path=output,
|
||||
file=output,
|
||||
progress_callback=self.download_progress_callback)
|
||||
print('Media downloaded to {}!'.format(output))
|
||||
|
||||
|
@ -275,7 +275,7 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
|
||||
@staticmethod
|
||||
def update_handler(update_object):
|
||||
if type(update_object) is UpdateShortMessage:
|
||||
if isinstance(update_object, UpdateShortMessage):
|
||||
if update_object.out:
|
||||
sprint('You sent {} to user #{}'.format(
|
||||
update_object.message, update_object.user_id))
|
||||
|
@ -283,7 +283,7 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
sprint('[User #{} sent {}]'.format(
|
||||
update_object.user_id, update_object.message))
|
||||
|
||||
elif type(update_object) is UpdateShortChatMessage:
|
||||
elif isinstance(update_object, UpdateShortChatMessage):
|
||||
if update_object.out:
|
||||
sprint('You sent {} to chat #{}'.format(
|
||||
update_object.message, update_object.chat_id))
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import re
|
||||
from zlib import crc32
|
||||
|
||||
|
||||
class TLObject:
|
||||
|
@ -24,12 +25,18 @@ class TLObject:
|
|||
self.namespace = None
|
||||
self.name = fullname
|
||||
|
||||
# The ID should be an hexadecimal string
|
||||
self.id = int(object_id, base=16)
|
||||
self.args = args
|
||||
self.result = result
|
||||
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
|
||||
def from_tl(tl, is_function):
|
||||
"""Returns a TL object from the given TL scheme line"""
|
||||
|
@ -38,8 +45,10 @@ class TLObject:
|
|||
match = re.match(r'''
|
||||
^ # We want to match from the beginning to the end
|
||||
([\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
|
||||
)? # If no constructor ID was given, CRC32 the 'tl' to determine it
|
||||
|
||||
(?:\s # After that, we want to match its arguments (name:type)
|
||||
{? # 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"""
|
||||
return self.id in TLObject.CORE_TYPES
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self, ignore_id=False):
|
||||
fullname = ('{}.{}'.format(self.namespace, self.name)
|
||||
if self.namespace is not None else self.name)
|
||||
|
||||
hex_id = hex(self.id)[2:].rjust(8,
|
||||
'0') # Skip 0x and add 0's for padding
|
||||
if getattr(self, 'id', None) is None or ignore_id:
|
||||
hex_id = ''
|
||||
else:
|
||||
# Skip 0x and add 0's for padding
|
||||
hex_id = '#' + hex(self.id)[2:].rjust(8, '0')
|
||||
|
||||
return '{}#{} {} = {}'.format(
|
||||
fullname, hex_id, ' '.join([str(arg) for arg in self.args]),
|
||||
self.result)
|
||||
if self.args:
|
||||
args = ' ' + ' '.join([repr(arg) for arg in self.args])
|
||||
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):
|
||||
fullname = ('{}.{}'.format(self.namespace, self.name)
|
||||
|
@ -214,3 +246,9 @@ class TLArg:
|
|||
return '{{{}:{}}}'.format(self.name, real_type)
|
||||
else:
|
||||
return '{}:{}'.format(self.name, real_type)
|
||||
|
||||
def __repr__(self):
|
||||
# Get rid of our special type
|
||||
return str(self)\
|
||||
.replace(':date', ':int')\
|
||||
.replace('?date', '?int')
|
||||
|
|
|
@ -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;
|
||||
|
||||
ipPort ipv4:int port:int = IpPort;
|
||||
help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector<ipPort> = help.ConfigSimple;
|
||||
|
||||
---functions---
|
||||
|
||||
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;
|
||||
|
||||
inputMediaEmpty#9664f57f = InputMedia;
|
||||
inputMediaUploadedPhoto#630c9af1 flags:# file:InputFile caption:string stickers:flags.0?Vector<InputDocument> = InputMedia;
|
||||
inputMediaPhoto#e9bfb4f3 id:InputPhoto caption:string = InputMedia;
|
||||
inputMediaUploadedPhoto#2f37e231 flags:# file:InputFile caption:string stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||
inputMediaPhoto#81fa373a flags:# id:InputPhoto caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
|
||||
inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia;
|
||||
inputMediaUploadedDocument#d070f1e9 flags:# file:InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> = InputMedia;
|
||||
inputMediaUploadedThumbDocument#50d88cae flags:# file:InputFile thumb:InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> = InputMedia;
|
||||
inputMediaDocument#1a77f29c id:InputDocument caption:string = 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;
|
||||
inputMediaDocument#5acb668e flags:# id:InputDocument caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaVenue#2827a81a geo_point:InputGeoPoint title:string address:string provider:string venue_id:string = InputMedia;
|
||||
inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
|
||||
inputMediaPhotoExternal#b55f4f18 url:string caption:string = InputMedia;
|
||||
inputMediaDocumentExternal#e5e9607c url:string caption:string = InputMedia;
|
||||
inputMediaPhotoExternal#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
|
||||
inputMediaInvoice#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia;
|
||||
|
||||
|
@ -216,11 +218,11 @@ userStatusLastMonth#77ebc742 = UserStatus;
|
|||
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;
|
||||
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;
|
||||
channelForbidden#8537784f flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title: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#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;
|
||||
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;
|
||||
chatParticipantCreator#da13538a user_id:int = ChatParticipant;
|
||||
|
@ -233,15 +235,15 @@ chatPhotoEmpty#37c1011c = ChatPhoto;
|
|||
chatPhoto#6153276a photo_small:FileLocation photo_big:FileLocation = ChatPhoto;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:string user_id:int = 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;
|
||||
messageMediaVenue#7912b71f geo:GeoPoint title:string address:string provider:string venue_id:string = 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;
|
||||
messageActionPaymentSent#40699cd0 currency:string total_amount:long = 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;
|
||||
|
||||
|
@ -326,7 +329,7 @@ contacts.link#3ace484c my_link:ContactLink foreign_link:ContactLink user:User =
|
|||
contacts.contactsNotModified#b74ba9d2 = 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.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;
|
||||
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;
|
||||
updateLangPackTooLong#10c2404b = Update;
|
||||
updateLangPack#56022f4d difference:LangPackDifference = Update;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -644,19 +649,16 @@ channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:
|
|||
|
||||
channelParticipant#15ebac1d user_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;
|
||||
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;
|
||||
channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter;
|
||||
channelParticipantsKicked#3c37bb7a = ChannelParticipantsFilter;
|
||||
channelParticipantsKicked#a3b54985 q:string = ChannelParticipantsFilter;
|
||||
channelParticipantsBots#b0d1865b = ChannelParticipantsFilter;
|
||||
|
||||
channelRoleEmpty#b285a0c6 = ChannelParticipantRole;
|
||||
channelRoleModerator#9618d975 = ChannelParticipantRole;
|
||||
channelRoleEditor#820bfe8c = ChannelParticipantRole;
|
||||
channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter;
|
||||
channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter;
|
||||
|
||||
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;
|
||||
|
||||
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.codeTypeCall#741cd3e3 = auth.CodeType;
|
||||
|
@ -725,6 +727,7 @@ topPeerCategoryBotsInline#148677e2 = TopPeerCategory;
|
|||
topPeerCategoryCorrespondents#637b7ed = TopPeerCategory;
|
||||
topPeerCategoryGroups#bd17a14a = TopPeerCategory;
|
||||
topPeerCategoryChannels#161d9628 = TopPeerCategory;
|
||||
topPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory;
|
||||
|
||||
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;
|
||||
pageBlockSlideshow#130c8963 items:Vector<PageBlock> caption:RichText = 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;
|
||||
pageFull#d7a19d69 blocks:Vector<PageBlock> photos:Vector<Photo> videos:Vector<Document> = Page;
|
||||
pagePart#8e3f9ebe blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> = Page;
|
||||
pageFull#556ec7aa blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> = Page;
|
||||
|
||||
phoneCallDiscardReasonMissed#85e42301 = 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;
|
||||
|
||||
inputStickerSetItem#ffa0a496 flags:# document:InputDocument emoji:string mask_coords:flags.0?MaskCoords = InputStickerSetItem;
|
||||
|
||||
inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall;
|
||||
|
||||
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;
|
||||
|
||||
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---
|
||||
|
||||
invokeAfterMsg#cb9f372d {X:Type} msg_id: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;
|
||||
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.search#11f812d8 q:string limit:int = contacts.Found;
|
||||
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;
|
||||
|
||||
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.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.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;
|
||||
|
@ -1023,6 +1066,8 @@ messages.reorderPinnedDialogs#959ff644 flags:# force:flags.0?true order:Vector<I
|
|||
messages.getPinnedDialogs#e254d64e = messages.PeerDialogs;
|
||||
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.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.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.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
|
||||
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.getNearestDc#1fb33026 = NearestDc;
|
||||
|
@ -1062,7 +1108,7 @@ channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;
|
|||
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.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.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates;
|
||||
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.leaveChannel#f836aa95 channel:InputChannel = 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.deleteChannel#c0111fe3 channel:InputChannel = 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.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates;
|
||||
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.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.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.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;
|
||||
|
@ -1098,4 +1150,9 @@ phone.discardCall#78d413a6 peer:InputPhoneCall duration:int reason:PhoneCallDisc
|
|||
phone.setCallRating#1c536a34 peer:InputPhoneCall rating:int comment:string = Updates;
|
||||
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
|
||||
|
|
161
telethon_generator/tl_generator.py
Executable file → Normal file
161
telethon_generator/tl_generator.py
Executable file → Normal file
|
@ -1,51 +1,46 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from zlib import crc32
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
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/
|
||||
from .parser import SourceBuilder, TLParser
|
||||
|
||||
|
||||
class TLGenerator:
|
||||
@staticmethod
|
||||
def tlobjects_exist():
|
||||
def __init__(self, output_dir):
|
||||
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
|
||||
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():
|
||||
def clean_tlobjects(self):
|
||||
"""Cleans the automatically generated TLObjects from disk"""
|
||||
if os.path.isdir(get_output_path('functions')):
|
||||
shutil.rmtree(get_output_path('functions'))
|
||||
for name in ('functions', 'types', 'all_tlobjects.py'):
|
||||
self._rm_if_exists(name)
|
||||
|
||||
if os.path.isdir(get_output_path('types')):
|
||||
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):
|
||||
def generate_tlobjects(self, scheme_file, import_depth):
|
||||
"""Generates all the TLObjects from scheme.tl to
|
||||
tl/functions and tl/types
|
||||
"""
|
||||
|
||||
# First ensure that the required parent directories exist
|
||||
os.makedirs(get_output_path('functions'), exist_ok=True)
|
||||
os.makedirs(get_output_path('types'), exist_ok=True)
|
||||
os.makedirs(self._get_file('functions'), exist_ok=True)
|
||||
os.makedirs(self._get_file('types'), exist_ok=True)
|
||||
|
||||
# Step 0: Cache the parsed file on a tuple
|
||||
tlobjects = tuple(TLParser.parse_file(scheme_file))
|
||||
|
@ -91,11 +86,11 @@ class TLGenerator:
|
|||
continue
|
||||
|
||||
# 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')
|
||||
|
||||
# Path depth to perform relative import
|
||||
depth = output_base_depth
|
||||
depth = import_depth
|
||||
if tlobject.namespace:
|
||||
depth += 1
|
||||
out_dir = os.path.join(out_dir, tlobject.namespace)
|
||||
|
@ -121,19 +116,19 @@ class TLGenerator:
|
|||
tlobject, builder, depth, type_constructors)
|
||||
|
||||
# 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:
|
||||
file.write('from . import {}\n'
|
||||
.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:
|
||||
file.write('from . import {}\n'
|
||||
.format(', '.join(type_namespaces)))
|
||||
|
||||
# Step 4: Once all the objects have been generated,
|
||||
# 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 SourceBuilder(file) as builder:
|
||||
builder.writeln(
|
||||
|
@ -182,17 +177,27 @@ class TLGenerator:
|
|||
importing and documentation strings.
|
||||
'"""
|
||||
|
||||
# Both types and functions inherit from
|
||||
# MTProtoRequest so they all can be sent
|
||||
builder.writeln('from {}.tl.mtproto_request import MTProtoRequest'
|
||||
# Both types and functions inherit from the TLObject class so they
|
||||
# all can be serialized and sent, however, only the functions are
|
||||
# "content_related".
|
||||
builder.writeln('from {}.tl.tlobject import TLObject'
|
||||
.format('.' * depth))
|
||||
|
||||
if tlobject.is_function and \
|
||||
any(a for a in tlobject.args if a.type == 'InputPeer'):
|
||||
# We can automatically convert a normal peer to an InputPeer,
|
||||
# it will make invoking a lot of requests a lot simpler.
|
||||
builder.writeln('from {}.utils import get_input_peer'
|
||||
.format('.' * depth))
|
||||
if tlobject.is_function:
|
||||
util_imports = set()
|
||||
for a in tlobject.args:
|
||||
# We can automatically convert some "full" types to
|
||||
# "input only" (like User -> InputPeerUser, etc.)
|
||||
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):
|
||||
# Currently only 'random_id' needs 'os' to be imported
|
||||
|
@ -200,7 +205,7 @@ class TLGenerator:
|
|||
|
||||
builder.writeln()
|
||||
builder.writeln()
|
||||
builder.writeln('class {}(MTProtoRequest):'.format(
|
||||
builder.writeln('class {}(TLObject):'.format(
|
||||
TLGenerator.get_class_name(tlobject)))
|
||||
|
||||
# Write the original .tl definition,
|
||||
|
@ -264,7 +269,7 @@ class TLGenerator:
|
|||
builder.write(' Must be a list.'.format(arg.name))
|
||||
|
||||
if arg.is_generic:
|
||||
builder.write(' Must be another MTProtoRequest.')
|
||||
builder.write(' Must be another TLObject request.')
|
||||
|
||||
builder.writeln()
|
||||
|
||||
|
@ -296,7 +301,7 @@ class TLGenerator:
|
|||
if tlobject.is_function:
|
||||
builder.writeln('self.result = None')
|
||||
builder.writeln(
|
||||
'self.confirmed = True # Confirmed by default')
|
||||
'self.content_related = True')
|
||||
|
||||
# Set the arguments
|
||||
if args:
|
||||
|
@ -317,10 +322,15 @@ class TLGenerator:
|
|||
)
|
||||
else:
|
||||
raise ValueError('Cannot infer a value for ', arg)
|
||||
|
||||
# Well-known cases, auto-cast it to the right type
|
||||
elif arg.type == 'InputPeer' and tlobject.is_function:
|
||||
# Well-known case, auto-cast it to the right type
|
||||
builder.writeln(
|
||||
'self.{0} = get_input_peer({0})'.format(arg.name))
|
||||
TLGenerator.write_get_input(builder, arg, 'get_input_peer')
|
||||
elif arg.type == 'InputChannel' and tlobject.is_function:
|
||||
TLGenerator.write_get_input(builder, arg, 'get_input_channel')
|
||||
elif arg.type == 'InputUser' and tlobject.is_function:
|
||||
TLGenerator.write_get_input(builder, arg, 'get_input_user')
|
||||
|
||||
else:
|
||||
builder.writeln('self.{0} = {0}'.format(arg.name))
|
||||
|
||||
|
@ -413,9 +423,28 @@ class TLGenerator:
|
|||
builder.end_block()
|
||||
|
||||
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
|
||||
|
||||
@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
|
||||
def get_class_name(tlobject):
|
||||
|
@ -475,11 +504,10 @@ class TLGenerator:
|
|||
"writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID")
|
||||
|
||||
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
|
||||
arg.is_vector = False
|
||||
TLGenerator.write_onsend_code(
|
||||
builder, arg, args, name='{}_item'.format(arg.name))
|
||||
TLGenerator.write_onsend_code(builder, arg, args, name='_x')
|
||||
arg.is_vector = True
|
||||
|
||||
elif arg.flag_indicator:
|
||||
|
@ -570,13 +598,12 @@ class TLGenerator:
|
|||
builder.writeln("reader.read_int() # Vector's constructor ID")
|
||||
|
||||
builder.writeln('{} = [] # Initialize an empty list'.format(name))
|
||||
builder.writeln('{}_len = reader.read_int()'.format(arg.name))
|
||||
builder.writeln('for _ in range({}_len):'.format(arg.name))
|
||||
builder.writeln('_len = reader.read_int()')
|
||||
builder.writeln('for _ in range(_len):')
|
||||
# Temporary disable .is_vector, not to enter this if again
|
||||
arg.is_vector = False
|
||||
TLGenerator.write_onresponse_code(
|
||||
builder, arg, args, name='{}_item'.format(arg.name))
|
||||
builder.writeln('{}.append({}_item)'.format(name, arg.name))
|
||||
TLGenerator.write_onresponse_code(builder, arg, args, name='_x')
|
||||
builder.writeln('{}.append(_x)'.format(name))
|
||||
arg.is_vector = True
|
||||
|
||||
elif arg.flag_indicator:
|
||||
|
@ -591,12 +618,14 @@ class TLGenerator:
|
|||
builder.writeln('{} = reader.read_long()'.format(name))
|
||||
|
||||
elif 'int128' == arg.type:
|
||||
builder.writeln('{} = reader.read_large_int(bits=128)'.format(
|
||||
name))
|
||||
builder.writeln(
|
||||
'{} = reader.read_large_int(bits=128)'.format(name)
|
||||
)
|
||||
|
||||
elif 'int256' == arg.type:
|
||||
builder.writeln('{} = reader.read_large_int(bits=256)'.format(
|
||||
name))
|
||||
builder.writeln(
|
||||
'{} = reader.read_large_int(bits=256)'.format(name)
|
||||
)
|
||||
|
||||
elif 'double' == arg.type:
|
||||
builder.writeln('{} = reader.read_double()'.format(name))
|
||||
|
@ -658,13 +687,3 @@ class TLGenerator:
|
|||
builder.writeln('self.result = reader.tgread_vector()')
|
||||
else:
|
||||
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.')
|
||||
|
|
Loading…
Reference in New Issue
Block a user