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
|
sudo -H pip install telethon
|
||||||
|
|
||||||
You're ready to go. Oh, and upgrading is just as easy:
|
If you get something like "SyntaxError: invalid syntax" on the ``from error``
|
||||||
|
line, it's because ``pip`` defaults to Python 2. Use `pip3` instead.
|
||||||
|
|
||||||
.. code:: sh
|
If you already have Telethon installed,
|
||||||
|
upgrade with ``pip install --upgrade telethon``!
|
||||||
sudo -H pip install --upgrade telethon
|
|
||||||
|
|
||||||
Installing Telethon manually
|
Installing Telethon manually
|
||||||
----------------------------
|
----------------------------
|
||||||
|
@ -71,7 +71,7 @@ Installing Telethon manually
|
||||||
(`GitHub <https://github.com/ricmoo/pyaes>`_, `package index <https://pypi.python.org/pypi/pyaes>`_)
|
(`GitHub <https://github.com/ricmoo/pyaes>`_, `package index <https://pypi.python.org/pypi/pyaes>`_)
|
||||||
2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git``
|
2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git``
|
||||||
3. Enter the cloned repository: ``cd Telethon``
|
3. Enter the cloned repository: ``cd Telethon``
|
||||||
4. Run the code generator: ``cd telethon_generator && python3 tl_generator.py``
|
4. Run the code generator: ``python3 setup.py gen_tl``
|
||||||
5. Done!
|
5. Done!
|
||||||
|
|
||||||
Running Telethon
|
Running Telethon
|
||||||
|
@ -160,13 +160,16 @@ The ``TelegramClient`` class should be used to provide a quick, well-documented
|
||||||
It is **not** meant to be a place for *all* the available Telegram ``Request``'s, because there are simply too many.
|
It is **not** meant to be a place for *all* the available Telegram ``Request``'s, because there are simply too many.
|
||||||
|
|
||||||
However, this doesn't mean that you cannot ``invoke`` all the power of Telegram's API.
|
However, this doesn't mean that you cannot ``invoke`` all the power of Telegram's API.
|
||||||
Whenever you need to ``invoke`` a Telegram ``Request``, all you need to do is the following:
|
Whenever you need to ``call`` a Telegram ``Request``, all you need to do is the following:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
result = client(SomeRequest(...))
|
||||||
|
|
||||||
|
# Or the old way:
|
||||||
result = client.invoke(SomeRequest(...))
|
result = client.invoke(SomeRequest(...))
|
||||||
|
|
||||||
You have just ``invoke``'d ``SomeRequest`` and retrieved its ``result``! That wasn't hard at all, was it?
|
You have just called ``SomeRequest`` and retrieved its ``result``! That wasn't hard at all, was it?
|
||||||
Now you may wonder, what's the deal with *all the power of Telegram's API*? Have a look under ``tl/functions/``.
|
Now you may wonder, what's the deal with *all the power of Telegram's API*? Have a look under ``tl/functions/``.
|
||||||
That is *everything* you can do. You have **over 200 API** ``Request``'s at your disposal.
|
That is *everything* you can do. You have **over 200 API** ``Request``'s at your disposal.
|
||||||
|
|
||||||
|
@ -232,7 +235,7 @@ Have you found a more updated version of the ``scheme.tl`` file? Those are great
|
||||||
as grabbing the
|
as grabbing the
|
||||||
`latest version <https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/scheme.tl>`_
|
`latest version <https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/scheme.tl>`_
|
||||||
and replacing the one you can find in this same directory by the updated one.
|
and replacing the one you can find in this same directory by the updated one.
|
||||||
Don't forget to run ``python3 tl_generator.py``.
|
Don't forget to run ``python3 setup.py gen_tl``.
|
||||||
|
|
||||||
If the changes weren't too big, everything should still work the same way as it did before; but with extra features.
|
If the changes weren't too big, everything should still work the same way as it did before; but with extra features.
|
||||||
|
|
||||||
|
|
|
@ -75,12 +75,19 @@ def get_create_path_for(tlobject):
|
||||||
return os.path.join(out_dir, get_file_name(tlobject, add_extension=True))
|
return os.path.join(out_dir, get_file_name(tlobject, add_extension=True))
|
||||||
|
|
||||||
|
|
||||||
|
def is_core_type(type_):
|
||||||
|
"""Returns "true" if the type is considered a core type"""
|
||||||
|
return type_.lower() in {
|
||||||
|
'int', 'long', 'int128', 'int256', 'double',
|
||||||
|
'vector', 'string', 'bool', 'true', 'bytes', 'date'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_path_for_type(type_, relative_to='.'):
|
def get_path_for_type(type_, relative_to='.'):
|
||||||
"""Similar to getting the path for a TLObject, it might not be possible
|
"""Similar to getting the path for a TLObject, it might not be possible
|
||||||
to have the TLObject itself but rather its name (the type);
|
to have the TLObject itself but rather its name (the type);
|
||||||
this method works in the same way, returning a relative path"""
|
this method works in the same way, returning a relative path"""
|
||||||
if type_.lower() in {'int', 'long', 'int128', 'int256', 'double',
|
if is_core_type(type_):
|
||||||
'vector', 'string', 'bool', 'true', 'bytes', 'date'}:
|
|
||||||
path = 'index.html#%s' % type_.lower()
|
path = 'index.html#%s' % type_.lower()
|
||||||
|
|
||||||
elif '.' in type_:
|
elif '.' in type_:
|
||||||
|
@ -396,11 +403,39 @@ def generate_documentation(scheme_file):
|
||||||
docs.add_row(get_class_name(func), link=link)
|
docs.add_row(get_class_name(func), link=link)
|
||||||
docs.end_table()
|
docs.end_table()
|
||||||
|
|
||||||
|
# List all the methods which take this type as input
|
||||||
|
docs.write_title('Methods accepting this type as input', level=3)
|
||||||
|
other_methods = sorted(
|
||||||
|
(t for t in tlobjects
|
||||||
|
if any(tltype == a.type for a in t.args) and t.is_function),
|
||||||
|
key=lambda t: t.name
|
||||||
|
)
|
||||||
|
if not other_methods:
|
||||||
|
docs.write_text(
|
||||||
|
'No methods accept this type as an input parameter.')
|
||||||
|
elif len(other_methods) == 1:
|
||||||
|
docs.write_text(
|
||||||
|
'Only this method has a parameter with this type.')
|
||||||
|
else:
|
||||||
|
docs.write_text(
|
||||||
|
'The following %d methods accept this type as an input '
|
||||||
|
'parameter.' % len(other_methods))
|
||||||
|
|
||||||
|
docs.begin_table(2)
|
||||||
|
for ot in other_methods:
|
||||||
|
link = get_create_path_for(ot)
|
||||||
|
link = get_relative_path(link, relative_to=filename)
|
||||||
|
docs.add_row(get_class_name(ot), link=link)
|
||||||
|
docs.end_table()
|
||||||
|
|
||||||
# List every other type which has this type as a member
|
# List every other type which has this type as a member
|
||||||
docs.write_title('Other types containing this type', level=3)
|
docs.write_title('Other types containing this type', level=3)
|
||||||
other_types = sorted((t for t in tlobjects
|
other_types = sorted(
|
||||||
if any(tltype == a.type for a in t.args)),
|
(t for t in tlobjects
|
||||||
key=lambda t: t.name)
|
if any(tltype == a.type for a in t.args)
|
||||||
|
and not t.is_function
|
||||||
|
), key=lambda t: t.name
|
||||||
|
)
|
||||||
|
|
||||||
if not other_types:
|
if not other_types:
|
||||||
docs.write_text(
|
docs.write_text(
|
||||||
|
@ -433,20 +468,30 @@ def generate_documentation(scheme_file):
|
||||||
layer = TLParser.find_layer(scheme_file)
|
layer = TLParser.find_layer(scheme_file)
|
||||||
types = set()
|
types = set()
|
||||||
methods = []
|
methods = []
|
||||||
|
constructors = []
|
||||||
for tlobject in tlobjects:
|
for tlobject in tlobjects:
|
||||||
if tlobject.is_function:
|
if tlobject.is_function:
|
||||||
methods.append(tlobject)
|
methods.append(tlobject)
|
||||||
|
else:
|
||||||
|
constructors.append(tlobject)
|
||||||
|
|
||||||
types.add(tlobject.result)
|
if not is_core_type(tlobject.result):
|
||||||
|
if re.search('^vector<', tlobject.result, re.IGNORECASE):
|
||||||
|
types.add(tlobject.result.split('<')[1].strip('>'))
|
||||||
|
else:
|
||||||
|
types.add(tlobject.result)
|
||||||
|
|
||||||
types = sorted(types)
|
types = sorted(types)
|
||||||
methods = sorted(methods, key=lambda m: m.name)
|
methods = sorted(methods, key=lambda m: m.name)
|
||||||
|
constructors = sorted(constructors, key=lambda c: c.name)
|
||||||
|
|
||||||
request_names = ', '.join('"' + get_class_name(m) + '"' for m in methods)
|
request_names = ', '.join('"' + get_class_name(m) + '"' for m in methods)
|
||||||
type_names = ', '.join('"' + get_class_name(t) + '"' for t in types)
|
type_names = ', '.join('"' + get_class_name(t) + '"' for t in types)
|
||||||
|
constructor_names = ', '.join('"' + get_class_name(t) + '"' for t in constructors)
|
||||||
|
|
||||||
request_urls = ', '.join('"' + get_create_path_for(m) + '"' for m in methods)
|
request_urls = ', '.join('"' + get_create_path_for(m) + '"' for m in methods)
|
||||||
type_urls = ', '.join('"' + get_path_for_type(t) + '"' for t in types)
|
type_urls = ', '.join('"' + get_path_for_type(t) + '"' for t in types)
|
||||||
|
constructor_urls = ', '.join('"' + get_create_path_for(t) + '"' for t in constructors)
|
||||||
|
|
||||||
replace_dict = {
|
replace_dict = {
|
||||||
'type_count': len(types),
|
'type_count': len(types),
|
||||||
|
@ -456,8 +501,10 @@ def generate_documentation(scheme_file):
|
||||||
|
|
||||||
'request_names': request_names,
|
'request_names': request_names,
|
||||||
'type_names': type_names,
|
'type_names': type_names,
|
||||||
|
'constructor_names': constructor_names,
|
||||||
'request_urls': request_urls,
|
'request_urls': request_urls,
|
||||||
'type_urls': type_urls
|
'type_urls': type_urls,
|
||||||
|
'constructor_urls': constructor_urls
|
||||||
}
|
}
|
||||||
|
|
||||||
with open('../res/core.html') as infile:
|
with open('../res/core.html') as infile:
|
||||||
|
|
|
@ -14,11 +14,26 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="main_div">
|
<div id="main_div">
|
||||||
|
<!-- You can append '?q=query' to the URL to default to a search -->
|
||||||
<input id="searchBox" type="text" onkeyup="updateSearch()"
|
<input id="searchBox" type="text" onkeyup="updateSearch()"
|
||||||
placeholder="Search for requests and types…" />
|
placeholder="Search for requests and types…" />
|
||||||
|
|
||||||
<div id="searchDiv">
|
<div id="searchDiv">
|
||||||
<table id="searchTable"></table>
|
|
||||||
|
<details open><summary class="title">Methods (<span id="methodsCount">0</span>)</summary>
|
||||||
|
<ul id="methodsList" class="together">
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open><summary class="title">Types (<span id="typesCount">0</span>)</summary>
|
||||||
|
<ul id="typesList" class="together">
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary class="title">Constructors (<span id="constructorsCount">0</span>)</summary>
|
||||||
|
<ul id="constructorsList" class="together">
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="contentDiv">
|
<div id="contentDiv">
|
||||||
|
@ -146,7 +161,6 @@
|
||||||
<pre><span class="sh3">#!/usr/bin/python3</span>
|
<pre><span class="sh3">#!/usr/bin/python3</span>
|
||||||
<span class="sh4">from</span> telethon <span class="sh4">import</span> TelegramClient
|
<span class="sh4">from</span> telethon <span class="sh4">import</span> TelegramClient
|
||||||
<span class="sh4">from</span> telethon.tl.functions.messages <span class="sh4">import</span> GetHistoryRequest
|
<span class="sh4">from</span> telethon.tl.functions.messages <span class="sh4">import</span> GetHistoryRequest
|
||||||
<span class="sh4">from</span> telethon.utils <span class="sh4">import</span> get_input_peer
|
|
||||||
|
|
||||||
<span class="sh3"># <b>(1)</b> Use your own values here</span>
|
<span class="sh3"># <b>(1)</b> Use your own values here</span>
|
||||||
api_id = <span class="sh1">12345</span>
|
api_id = <span class="sh1">12345</span>
|
||||||
|
@ -167,25 +181,28 @@ dialogs, entities = client.get_dialogs(<span class="sh1">10</span>)
|
||||||
entity = entities[<span class="sh1">0</span>]
|
entity = entities[<span class="sh1">0</span>]
|
||||||
|
|
||||||
<span class="sh3"># <b>(4)</b> !! Invoking a request manually !!</span>
|
<span class="sh3"># <b>(4)</b> !! Invoking a request manually !!</span>
|
||||||
result = <b>client.invoke</b>(
|
result = <b>client</b>(GetHistoryRequest(
|
||||||
GetHistoryRequest(
|
entity,
|
||||||
get_input_peer(entity),
|
limit=<span class="sh1">20</span>,
|
||||||
limit=<span class="sh1">20</span>,
|
offset_date=<span class="sh1">None</span>,
|
||||||
offset_date=<span class="sh1">None</span>,
|
offset_id=<span class="sh1">0</span>,
|
||||||
offset_id=<span class="sh1">0</span>,
|
max_id=<span class="sh1">0</span>,
|
||||||
max_id=<span class="sh1">0</span>,
|
min_id=<span class="sh1">0</span>,
|
||||||
min_id=<span class="sh1">0</span>,
|
add_offset=<span class="sh1">0</span>
|
||||||
add_offset=<span class="sh1">0</span>))
|
))
|
||||||
|
|
||||||
<span class="sh3"># Now you have access to the first 20 messages</span>
|
<span class="sh3"># Now you have access to the first 20 messages</span>
|
||||||
messages = result.messages</pre>
|
messages = result.messages</pre>
|
||||||
|
|
||||||
<p>As it can be seen, manually invoking requests with
|
<p>As it can be seen, manually calling requests with
|
||||||
<code>client.invoke()</code> is way more verbose than using the built-in
|
<code>client(request)</code> (or using the old way, by calling
|
||||||
methods (such as <code>client.get_dialogs()</code>. However, and given
|
<code>client.invoke(request)</code>) is way more verbose than using the
|
||||||
that there are so many methods available, it's impossible to provide a nice
|
built-in methods (such as <code>client.get_dialogs()</code>).</p>
|
||||||
interface to things that may change over time. To get full access, however,
|
|
||||||
you're still able to invoke these methods manually.</p>
|
<p>However, and
|
||||||
|
given that there are so many methods available, it's impossible to provide
|
||||||
|
a nice interface to things that may change over time. To get full access,
|
||||||
|
however, you're still able to invoke these methods manually.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -193,13 +210,66 @@ messages = result.messages</pre>
|
||||||
contentDiv = document.getElementById("contentDiv");
|
contentDiv = document.getElementById("contentDiv");
|
||||||
searchDiv = document.getElementById("searchDiv");
|
searchDiv = document.getElementById("searchDiv");
|
||||||
searchBox = document.getElementById("searchBox");
|
searchBox = document.getElementById("searchBox");
|
||||||
searchTable = document.getElementById("searchTable");
|
|
||||||
|
|
||||||
requests = [{request_names}];
|
// Search lists
|
||||||
types = [{type_names}];
|
methodsList = document.getElementById("methodsList");
|
||||||
|
methodsCount = document.getElementById("methodsCount");
|
||||||
|
|
||||||
requestsu = [{request_urls}];
|
typesList = document.getElementById("typesList");
|
||||||
typesu = [{type_urls}];
|
typesCount = document.getElementById("typesCount");
|
||||||
|
|
||||||
|
constructorsList = document.getElementById("constructorsList");
|
||||||
|
constructorsCount = document.getElementById("constructorsCount");
|
||||||
|
|
||||||
|
try {
|
||||||
|
requests = [{request_names}];
|
||||||
|
types = [{type_names}];
|
||||||
|
constructors = [{constructor_names}];
|
||||||
|
|
||||||
|
requestsu = [{request_urls}];
|
||||||
|
typesu = [{type_urls}];
|
||||||
|
constructorsu = [{constructor_urls}];
|
||||||
|
} catch (e) {
|
||||||
|
requests = [];
|
||||||
|
types = [];
|
||||||
|
constructors = [];
|
||||||
|
requestsu = [];
|
||||||
|
typesu = [];
|
||||||
|
constructorsu = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given two input arrays "original" and "original urls" and a query,
|
||||||
|
// return a pair of arrays with matching "query" elements from "original".
|
||||||
|
//
|
||||||
|
// TODO Perhaps return an array of pairs instead a pair of arrays (for cache).
|
||||||
|
function getSearchArray(original, originalu, query) {
|
||||||
|
var destination = [];
|
||||||
|
var destinationu = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < original.length; ++i) {
|
||||||
|
if (original[i].toLowerCase().indexOf(query) != -1) {
|
||||||
|
destination.push(original[i]);
|
||||||
|
destinationu.push(originalu[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [destination, destinationu];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify "countSpan" and "resultList" accordingly based on the elements
|
||||||
|
// given as [[elements], [element urls]] (both with the same length)
|
||||||
|
function buildList(countSpan, resultList, foundElements) {
|
||||||
|
var result = "";
|
||||||
|
for (var i = 0; i < foundElements[0].length; ++i) {
|
||||||
|
result += '<li>';
|
||||||
|
result += '<a href="' + foundElements[1][i] + '">';
|
||||||
|
result += foundElements[0][i];
|
||||||
|
result += '</a></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
countSpan.innerHTML = "" + foundElements[0].length;
|
||||||
|
resultList.innerHTML = result;
|
||||||
|
}
|
||||||
|
|
||||||
function updateSearch() {
|
function updateSearch() {
|
||||||
if (searchBox.value) {
|
if (searchBox.value) {
|
||||||
|
@ -207,52 +277,37 @@ function updateSearch() {
|
||||||
searchDiv.style.display = "";
|
searchDiv.style.display = "";
|
||||||
|
|
||||||
var query = searchBox.value.toLowerCase();
|
var query = searchBox.value.toLowerCase();
|
||||||
var foundRequests = [];
|
|
||||||
var foundRequestsu = [];
|
|
||||||
for (var i = 0; i < requests.length; ++i) {
|
|
||||||
if (requests[i].toLowerCase().indexOf(query) != -1) {
|
|
||||||
foundRequests.push(requests[i]);
|
|
||||||
foundRequestsu.push(requestsu[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var foundTypes = [];
|
var foundRequests = getSearchArray(requests, requestsu, query);
|
||||||
var foundTypesu = [];
|
var foundTypes = getSearchArray(types, typesu, query);
|
||||||
for (var i = 0; i < types.length; ++i) {
|
var foundConstructors = getSearchArray(
|
||||||
if (types[i].toLowerCase().indexOf(query) != -1) {
|
constructors, constructorsu, query
|
||||||
foundTypes.push(types[i]);
|
);
|
||||||
foundTypesu.push(typesu[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var top = foundRequests.length > foundTypes.length ?
|
buildList(methodsCount, methodsList, foundRequests);
|
||||||
foundRequests.length : foundTypes.length;
|
buildList(typesCount, typesList, foundTypes);
|
||||||
|
buildList(constructorsCount, constructorsList, foundConstructors);
|
||||||
result = "";
|
|
||||||
for (var i = 0; i <= top; ++i) {
|
|
||||||
result += "<tr><td>";
|
|
||||||
|
|
||||||
if (i < foundRequests.length) {
|
|
||||||
result +=
|
|
||||||
'<a href="'+foundRequestsu[i]+'">'+foundRequests[i]+'</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
result += "</td><td>";
|
|
||||||
|
|
||||||
if (i < foundTypes.length) {
|
|
||||||
result +=
|
|
||||||
'<a href="'+foundTypesu[i]+'">'+foundTypes[i]+'</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
result += "</td></tr>";
|
|
||||||
}
|
|
||||||
searchTable.innerHTML = result;
|
|
||||||
} else {
|
} else {
|
||||||
contentDiv.style.display = "";
|
contentDiv.style.display = "";
|
||||||
searchDiv.style.display = "none";
|
searchDiv.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getQuery(name) {
|
||||||
|
var query = window.location.search.substring(1);
|
||||||
|
var vars = query.split("&");
|
||||||
|
for (var i = 0; i != vars.length; ++i) {
|
||||||
|
var pair = vars[i].split("=");
|
||||||
|
if (pair[0] == name)
|
||||||
|
return pair[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = getQuery('q');
|
||||||
|
if (query) {
|
||||||
|
searchBox.value = query;
|
||||||
|
}
|
||||||
|
|
||||||
updateSearch();
|
updateSearch();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -52,7 +52,7 @@ table td {
|
||||||
margin: 0 8px -2px 0;
|
margin: 0 8px -2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1, summary.title {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +137,26 @@ button:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* https://www.w3schools.com/css/css_navbar.asp */
|
||||||
|
ul.together {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.together li {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.together li a {
|
||||||
|
display: block;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f0f4f8;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* https://stackoverflow.com/a/30810322 */
|
/* https://stackoverflow.com/a/30810322 */
|
||||||
.invisible {
|
.invisible {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -153,7 +173,7 @@ button:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
h1 {
|
h1, summary.title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
h3 {
|
h3 {
|
||||||
|
|
127
setup.py
Normal file → Executable file
127
setup.py
Normal file → Executable file
|
@ -1,87 +1,98 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
"""A setuptools based setup module.
|
"""A setuptools based setup module.
|
||||||
|
|
||||||
See:
|
See:
|
||||||
https://packaging.python.org/en/latest/distributing.html
|
https://packaging.python.org/en/latest/distributing.html
|
||||||
https://github.com/pypa/sampleproject
|
https://github.com/pypa/sampleproject
|
||||||
|
|
||||||
|
Extra supported commands are:
|
||||||
|
* gen_tl, to generate the classes required for Telethon to run
|
||||||
|
* clean_tl, to clean these generated classes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# To use a consistent encoding
|
# To use a consistent encoding
|
||||||
from codecs import open
|
from codecs import open
|
||||||
|
from sys import argv
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
# Always prefer setuptools over distutils
|
# Always prefer setuptools over distutils
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
from telethon import TelegramClient
|
try:
|
||||||
|
from telethon import TelegramClient
|
||||||
|
except ImportError:
|
||||||
|
TelegramClient = None
|
||||||
|
|
||||||
here = path.abspath(path.dirname(__file__))
|
|
||||||
|
|
||||||
# Get the long description from the README file
|
if __name__ == '__main__':
|
||||||
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
|
if len(argv) >= 2 and argv[1] == 'gen_tl':
|
||||||
long_description = f.read()
|
from telethon_generator.tl_generator import TLGenerator
|
||||||
|
generator = TLGenerator('telethon/tl')
|
||||||
|
if generator.tlobjects_exist():
|
||||||
|
print('Detected previous TLObjects. Cleaning...')
|
||||||
|
generator.clean_tlobjects()
|
||||||
|
|
||||||
setup(
|
print('Generating TLObjects...')
|
||||||
name='Telethon',
|
generator.generate_tlobjects(
|
||||||
|
'telethon_generator/scheme.tl', import_depth=2
|
||||||
|
)
|
||||||
|
print('Done.')
|
||||||
|
|
||||||
# Versions should comply with PEP440.
|
elif len(argv) >= 2 and argv[1] == 'clean_tl':
|
||||||
version=TelegramClient.__version__,
|
from telethon_generator.tl_generator import TLGenerator
|
||||||
description="Python3 Telegram's client implementation with full access to its API",
|
print('Cleaning...')
|
||||||
long_description=long_description,
|
TLGenerator('telethon/tl').clean_tlobjects()
|
||||||
|
print('Done.')
|
||||||
|
|
||||||
# The project's main homepage.
|
else:
|
||||||
url='https://github.com/LonamiWebs/Telethon',
|
if not TelegramClient:
|
||||||
download_url='https://github.com/LonamiWebs/Telethon/releases',
|
print('Run `python3', argv[0], 'gen_tl` first.')
|
||||||
|
quit()
|
||||||
|
|
||||||
# Author details
|
here = path.abspath(path.dirname(__file__))
|
||||||
author='Lonami Exo',
|
|
||||||
author_email='totufals@hotmail.com',
|
|
||||||
|
|
||||||
# Choose your license
|
# Get the long description from the README file
|
||||||
license='MIT',
|
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
setup(
|
||||||
classifiers=[
|
name='Telethon',
|
||||||
# How mature is this project? Common values are
|
|
||||||
# 3 - Alpha
|
|
||||||
# 4 - Beta
|
|
||||||
# 5 - Production/Stable
|
|
||||||
'Development Status :: 3 - Alpha',
|
|
||||||
|
|
||||||
# Indicate who your project is intended for
|
# Versions should comply with PEP440.
|
||||||
'Intended Audience :: Developers',
|
version=TelegramClient.__version__,
|
||||||
'Topic :: Communications :: Chat',
|
description="Full-featured Telegram client library for Python 3",
|
||||||
|
long_description=long_description,
|
||||||
|
|
||||||
# Pick your license as you wish (should match "license" above)
|
url='https://github.com/LonamiWebs/Telethon',
|
||||||
'License :: OSI Approved :: MIT License',
|
download_url='https://github.com/LonamiWebs/Telethon/releases',
|
||||||
|
|
||||||
# Specify the Python versions you support here. In particular, ensure
|
author='Lonami Exo',
|
||||||
# that you indicate whether you support Python 2, Python 3 or both.
|
author_email='totufals@hotmail.com',
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'Programming Language :: Python :: 3.3',
|
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Programming Language :: Python :: 3.6'
|
|
||||||
],
|
|
||||||
|
|
||||||
# What does your project relate to?
|
license='MIT',
|
||||||
keywords='Telegram API chat client MTProto',
|
|
||||||
|
|
||||||
# You can just specify the packages manually here if your project is
|
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||||
# simple. Or you can use find_packages().
|
classifiers=[
|
||||||
packages=find_packages(exclude=[
|
# 3 - Alpha
|
||||||
'telethon_generator', 'telethon_tests', 'run_tests.py',
|
# 4 - Beta
|
||||||
'try_telethon.py'
|
# 5 - Production/Stable
|
||||||
]),
|
'Development Status :: 3 - Alpha',
|
||||||
|
|
||||||
# List run-time dependencies here. These will be installed by pip when
|
'Intended Audience :: Developers',
|
||||||
# your project is installed.
|
'Topic :: Communications :: Chat',
|
||||||
install_requires=['pyaes'],
|
|
||||||
|
|
||||||
# To provide executable scripts, use entry points in preference to the
|
'License :: OSI Approved :: MIT License',
|
||||||
# "scripts" keyword. Entry points provide cross-platform support and allow
|
|
||||||
# pip to create the appropriate form of executable for the target platform.
|
'Programming Language :: Python :: 3',
|
||||||
entry_points={
|
'Programming Language :: Python :: 3.3',
|
||||||
'console_scripts': [
|
'Programming Language :: Python :: 3.4',
|
||||||
'gen_tl = tl_generator:clean_and_generate',
|
'Programming Language :: Python :: 3.5',
|
||||||
],
|
'Programming Language :: Python :: 3.6'
|
||||||
})
|
],
|
||||||
|
keywords='telegram api chat client library messaging mtproto',
|
||||||
|
packages=find_packages(exclude=[
|
||||||
|
'telethon_generator', 'telethon_tests', 'run_tests.py',
|
||||||
|
'try_telethon.py'
|
||||||
|
]),
|
||||||
|
install_requires=['pyaes']
|
||||||
|
)
|
||||||
|
|
|
@ -18,10 +18,10 @@ from .rpc_errors_420 import *
|
||||||
|
|
||||||
def rpc_message_to_error(code, message):
|
def rpc_message_to_error(code, message):
|
||||||
errors = {
|
errors = {
|
||||||
303: rpc_303_errors,
|
303: rpc_errors_303_all,
|
||||||
400: rpc_400_errors,
|
400: rpc_errors_400_all,
|
||||||
401: rpc_401_errors,
|
401: rpc_errors_401_all,
|
||||||
420: rpc_420_errors
|
420: rpc_errors_420_all
|
||||||
}.get(code, None)
|
}.get(code, None)
|
||||||
|
|
||||||
if errors is not None:
|
if errors is not None:
|
||||||
|
|
|
@ -43,7 +43,7 @@ class UserMigrateError(InvalidDCError):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
rpc_303_errors = {
|
rpc_errors_303_all = {
|
||||||
'FILE_MIGRATE_(\d+)': FileMigrateError,
|
'FILE_MIGRATE_(\d+)': FileMigrateError,
|
||||||
'PHONE_MIGRATE_(\d+)': PhoneMigrateError,
|
'PHONE_MIGRATE_(\d+)': PhoneMigrateError,
|
||||||
'NETWORK_MIGRATE_(\d+)': NetworkMigrateError,
|
'NETWORK_MIGRATE_(\d+)': NetworkMigrateError,
|
||||||
|
|
|
@ -44,6 +44,14 @@ class ChatIdInvalidError(BadRequestError):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionLangPackInvalid(BadRequestError):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(Exception, self).__init__(
|
||||||
|
self,
|
||||||
|
'The specified language pack is not valid.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConnectionLayerInvalidError(BadRequestError):
|
class ConnectionLayerInvalidError(BadRequestError):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(Exception, self).__init__(
|
super(Exception, self).__init__(
|
||||||
|
@ -321,7 +329,7 @@ class UserIdInvalidError(BadRequestError):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
rpc_400_errors = {
|
rpc_errors_400_all = {
|
||||||
'API_ID_INVALID': ApiIdInvalidError,
|
'API_ID_INVALID': ApiIdInvalidError,
|
||||||
'BOT_METHOD_INVALID': BotMethodInvalidError,
|
'BOT_METHOD_INVALID': BotMethodInvalidError,
|
||||||
'CHANNEL_INVALID': ChannelInvalidError,
|
'CHANNEL_INVALID': ChannelInvalidError,
|
||||||
|
|
|
@ -84,7 +84,7 @@ class UserDeactivatedError(UnauthorizedError):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
rpc_401_errors = {
|
rpc_errors_401_all = {
|
||||||
'ACTIVE_USER_REQUIRED': ActiveUserRequiredError,
|
'ACTIVE_USER_REQUIRED': ActiveUserRequiredError,
|
||||||
'AUTH_KEY_INVALID': AuthKeyInvalidError,
|
'AUTH_KEY_INVALID': AuthKeyInvalidError,
|
||||||
'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError,
|
'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError,
|
||||||
|
|
|
@ -11,6 +11,6 @@ class FloodWaitError(FloodError):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
rpc_420_errors = {
|
rpc_errors_420_all = {
|
||||||
'FLOOD_WAIT_(\d+)': FloodWaitError
|
'FLOOD_WAIT_(\d+)': FloodWaitError
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,9 +30,12 @@ class TcpClient:
|
||||||
else: # tuple, list, etc.
|
else: # tuple, list, etc.
|
||||||
self._socket.set_proxy(*self._proxy)
|
self._socket.set_proxy(*self._proxy)
|
||||||
|
|
||||||
def connect(self, ip, port):
|
def connect(self, ip, port, timeout):
|
||||||
"""Connects to the specified IP and port number"""
|
"""Connects to the specified IP and port number.
|
||||||
|
'timeout' must be given in seconds
|
||||||
|
"""
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
|
self._socket.settimeout(timeout)
|
||||||
self._socket.connect((ip, port))
|
self._socket.connect((ip, port))
|
||||||
self._socket.setblocking(False)
|
self._socket.setblocking(False)
|
||||||
self.connected = True
|
self.connected = True
|
||||||
|
@ -80,7 +83,7 @@ class TcpClient:
|
||||||
|
|
||||||
# Set the starting time so we can
|
# Set the starting time so we can
|
||||||
# calculate whether the timeout should fire
|
# calculate whether the timeout should fire
|
||||||
start_time = datetime.now() if timeout else None
|
start_time = datetime.now() if timeout is not None else None
|
||||||
|
|
||||||
with BufferedWriter(BytesIO(), buffer_size=size) as buffer:
|
with BufferedWriter(BytesIO(), buffer_size=size) as buffer:
|
||||||
bytes_left = size
|
bytes_left = size
|
||||||
|
@ -93,8 +96,9 @@ class TcpClient:
|
||||||
try:
|
try:
|
||||||
partial = self._socket.recv(bytes_left)
|
partial = self._socket.recv(bytes_left)
|
||||||
if len(partial) == 0:
|
if len(partial) == 0:
|
||||||
|
self.connected = False
|
||||||
raise ConnectionResetError(
|
raise ConnectionResetError(
|
||||||
'The server has closed the connection (recv() returned 0 bytes).')
|
'The server has closed the connection.')
|
||||||
|
|
||||||
buffer.write(partial)
|
buffer.write(partial)
|
||||||
bytes_left -= len(partial)
|
bytes_left -= len(partial)
|
||||||
|
@ -104,7 +108,7 @@ class TcpClient:
|
||||||
time.sleep(self.delay)
|
time.sleep(self.delay)
|
||||||
|
|
||||||
# Check if the timeout finished
|
# Check if the timeout finished
|
||||||
if timeout:
|
if timeout is not None:
|
||||||
time_passed = datetime.now() - start_time
|
time_passed = datetime.now() - start_time
|
||||||
if time_passed > timeout:
|
if time_passed > timeout:
|
||||||
raise TimeoutError(
|
raise TimeoutError(
|
||||||
|
|
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
|
import time
|
||||||
|
|
||||||
from ..extensions import BinaryReader, BinaryWriter
|
from ..extensions import BinaryReader, BinaryWriter
|
||||||
|
@ -42,17 +41,12 @@ class MtProtoPlainSender:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _get_new_msg_id(self):
|
def _get_new_msg_id(self):
|
||||||
"""Generates a new message ID based on the current time (in ms) since epoch"""
|
"""Generates a new message ID based on the current time since epoch"""
|
||||||
# See https://core.telegram.org/mtproto/description#message-identifier-msg-id
|
# See core.telegram.org/mtproto/description#message-identifier-msg-id
|
||||||
ms_time = int(time.time() * 1000)
|
now = time.time()
|
||||||
new_msg_id = (((ms_time // 1000) << 32)
|
nanoseconds = int((now - int(now)) * 1e+9)
|
||||||
| # "must approximately equal unix time*2^32"
|
# "message identifiers are divisible by 4"
|
||||||
((ms_time % 1000) << 22)
|
new_msg_id = (int(now) << 32) | (nanoseconds << 2)
|
||||||
| # "approximate moment in time the message was created"
|
|
||||||
random.randint(0, 524288)
|
|
||||||
<< 2) # "message identifiers are divisible by 4"
|
|
||||||
|
|
||||||
# Ensure that we always return a message ID which is higher than the previous one
|
|
||||||
if self._last_msg_id >= new_msg_id:
|
if self._last_msg_id >= new_msg_id:
|
||||||
new_msg_id = self._last_msg_id + 4
|
new_msg_id = self._last_msg_id + 4
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ class MtProtoSender:
|
||||||
"""MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)"""
|
"""MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)"""
|
||||||
|
|
||||||
def __init__(self, transport, session):
|
def __init__(self, transport, session):
|
||||||
self._transport = transport
|
self.transport = transport
|
||||||
self.session = session
|
self.session = session
|
||||||
self._logger = logging.getLogger(__name__)
|
self._logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -33,11 +33,14 @@ class MtProtoSender:
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Connects to the server"""
|
"""Connects to the server"""
|
||||||
self._transport.connect()
|
self.transport.connect()
|
||||||
|
|
||||||
|
def is_connected(self):
|
||||||
|
return self.transport.is_connected()
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
"""Disconnects from the server"""
|
"""Disconnects from the server"""
|
||||||
self._transport.close()
|
self.transport.close()
|
||||||
|
|
||||||
# region Send and receive
|
# region Send and receive
|
||||||
|
|
||||||
|
@ -73,11 +76,12 @@ class MtProtoSender:
|
||||||
|
|
||||||
del self._need_confirmation[:]
|
del self._need_confirmation[:]
|
||||||
|
|
||||||
def receive(self, request=None, timeout=timedelta(seconds=5), updates=None):
|
def receive(self, request=None, updates=None, **kwargs):
|
||||||
"""Receives the specified MTProtoRequest ("fills in it"
|
"""Receives the specified MTProtoRequest ("fills in it"
|
||||||
the received data). This also restores the updates thread.
|
the received data). This also restores the updates thread.
|
||||||
An optional timeout can be specified to cancel the operation
|
|
||||||
if no data has been read after its time delta.
|
An optional named parameter 'timeout' can be specified if
|
||||||
|
one desires to override 'self.transport.timeout'.
|
||||||
|
|
||||||
If 'request' is None, a single item will be read into
|
If 'request' is None, a single item will be read into
|
||||||
the 'updates' list (which cannot be None).
|
the 'updates' list (which cannot be None).
|
||||||
|
@ -96,8 +100,8 @@ class MtProtoSender:
|
||||||
# or, if there is no request, until we read an update
|
# or, if there is no request, until we read an update
|
||||||
while (request and not request.confirm_received) or \
|
while (request and not request.confirm_received) or \
|
||||||
(not request and not updates):
|
(not request and not updates):
|
||||||
self._logger.info('Trying to .receive() the request result...')
|
self._logger.debug('Trying to .receive() the request result...')
|
||||||
seq, body = self._transport.receive(timeout)
|
seq, body = self.transport.receive(**kwargs)
|
||||||
message, remote_msg_id, remote_seq = self._decode_msg(body)
|
message, remote_msg_id, remote_seq = self._decode_msg(body)
|
||||||
|
|
||||||
with BinaryReader(message) as reader:
|
with BinaryReader(message) as reader:
|
||||||
|
@ -110,21 +114,19 @@ class MtProtoSender:
|
||||||
self._pending_receive.remove(request)
|
self._pending_receive.remove(request)
|
||||||
except ValueError: pass
|
except ValueError: pass
|
||||||
|
|
||||||
self._logger.info('Request result received')
|
self._logger.debug('Request result received')
|
||||||
self._logger.debug('receive() released the lock')
|
self._logger.debug('receive() released the lock')
|
||||||
|
|
||||||
def receive_updates(self, timeout=timedelta(seconds=5)):
|
def receive_updates(self, **kwargs):
|
||||||
"""Receives one or more update objects
|
"""Wrapper for .receive(request=None, updates=[])"""
|
||||||
and returns them as a list
|
|
||||||
"""
|
|
||||||
updates = []
|
updates = []
|
||||||
self.receive(timeout=timeout, updates=updates)
|
self.receive(updates=updates, **kwargs)
|
||||||
return updates
|
return updates
|
||||||
|
|
||||||
def cancel_receive(self):
|
def cancel_receive(self):
|
||||||
"""Cancels any pending receive operation
|
"""Cancels any pending receive operation
|
||||||
by raising a ReadCancelledError"""
|
by raising a ReadCancelledError"""
|
||||||
self._transport.cancel_receive()
|
self.transport.cancel_receive()
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
@ -141,7 +143,7 @@ class MtProtoSender:
|
||||||
plain_writer.write_long(self.session.id, signed=False)
|
plain_writer.write_long(self.session.id, signed=False)
|
||||||
plain_writer.write_long(request.request_msg_id)
|
plain_writer.write_long(request.request_msg_id)
|
||||||
plain_writer.write_int(
|
plain_writer.write_int(
|
||||||
self.session.generate_sequence(request.confirmed))
|
self.session.generate_sequence(request.content_related))
|
||||||
|
|
||||||
plain_writer.write_int(len(packet))
|
plain_writer.write_int(len(packet))
|
||||||
plain_writer.write(packet)
|
plain_writer.write(packet)
|
||||||
|
@ -157,7 +159,7 @@ class MtProtoSender:
|
||||||
self.session.auth_key.key_id, signed=False)
|
self.session.auth_key.key_id, signed=False)
|
||||||
cipher_writer.write(msg_key)
|
cipher_writer.write(msg_key)
|
||||||
cipher_writer.write(cipher_text)
|
cipher_writer.write(cipher_text)
|
||||||
self._transport.send(cipher_writer.get_bytes())
|
self.transport.send(cipher_writer.get_bytes())
|
||||||
|
|
||||||
def _decode_msg(self, body):
|
def _decode_msg(self, body):
|
||||||
"""Decodes an received encrypted message body bytes"""
|
"""Decodes an received encrypted message body bytes"""
|
||||||
|
@ -224,10 +226,10 @@ class MtProtoSender:
|
||||||
ack = reader.tgread_object()
|
ack = reader.tgread_object()
|
||||||
for r in self._pending_receive:
|
for r in self._pending_receive:
|
||||||
if r.request_msg_id in ack.msg_ids:
|
if r.request_msg_id in ack.msg_ids:
|
||||||
self._logger.warning('Ack found for the a request')
|
self._logger.debug('Ack found for the a request')
|
||||||
|
|
||||||
if self.logging_out:
|
if self.logging_out:
|
||||||
self._logger.info('Message ack confirmed a request')
|
self._logger.debug('Message ack confirmed a request')
|
||||||
r.confirm_received = True
|
r.confirm_received = True
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -245,7 +247,7 @@ class MtProtoSender:
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self._logger.warning('Unknown message: {}'.format(hex(code)))
|
self._logger.debug('Unknown message: {}'.format(hex(code)))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
@ -255,13 +257,13 @@ class MtProtoSender:
|
||||||
def _handle_pong(self, msg_id, sequence, reader):
|
def _handle_pong(self, msg_id, sequence, reader):
|
||||||
self._logger.debug('Handling pong')
|
self._logger.debug('Handling pong')
|
||||||
reader.read_int(signed=False) # code
|
reader.read_int(signed=False) # code
|
||||||
received_msg_id = reader.read_long(signed=False)
|
received_msg_id = reader.read_long()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request = next(r for r in self._pending_receive
|
request = next(r for r in self._pending_receive
|
||||||
if r.request_msg_id == received_msg_id)
|
if r.request_msg_id == received_msg_id)
|
||||||
|
|
||||||
self._logger.warning('Pong confirmed a request')
|
self._logger.debug('Pong confirmed a request')
|
||||||
request.confirm_received = True
|
request.confirm_received = True
|
||||||
except StopIteration: pass
|
except StopIteration: pass
|
||||||
|
|
||||||
|
@ -272,23 +274,28 @@ class MtProtoSender:
|
||||||
reader.read_int(signed=False) # code
|
reader.read_int(signed=False) # code
|
||||||
size = reader.read_int()
|
size = reader.read_int()
|
||||||
for _ in range(size):
|
for _ in range(size):
|
||||||
inner_msg_id = reader.read_long(signed=False)
|
inner_msg_id = reader.read_long()
|
||||||
reader.read_int() # inner_sequence
|
reader.read_int() # inner_sequence
|
||||||
inner_length = reader.read_int()
|
inner_length = reader.read_int()
|
||||||
begin_position = reader.tell_position()
|
begin_position = reader.tell_position()
|
||||||
|
|
||||||
# Note that this code is IMPORTANT for skipping RPC results of
|
# Note that this code is IMPORTANT for skipping RPC results of
|
||||||
# lost requests (i.e., ones from the previous connection session)
|
# lost requests (i.e., ones from the previous connection session)
|
||||||
if not self._process_msg(
|
try:
|
||||||
inner_msg_id, sequence, reader, updates):
|
if not self._process_msg(
|
||||||
|
inner_msg_id, sequence, reader, updates):
|
||||||
|
reader.set_position(begin_position + inner_length)
|
||||||
|
except:
|
||||||
|
# If any error is raised, something went wrong; skip the packet
|
||||||
reader.set_position(begin_position + inner_length)
|
reader.set_position(begin_position + inner_length)
|
||||||
|
raise
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _handle_bad_server_salt(self, msg_id, sequence, reader):
|
def _handle_bad_server_salt(self, msg_id, sequence, reader):
|
||||||
self._logger.debug('Handling bad server salt')
|
self._logger.debug('Handling bad server salt')
|
||||||
reader.read_int(signed=False) # code
|
reader.read_int(signed=False) # code
|
||||||
bad_msg_id = reader.read_long(signed=False)
|
bad_msg_id = reader.read_long()
|
||||||
reader.read_int() # bad_msg_seq_no
|
reader.read_int() # bad_msg_seq_no
|
||||||
reader.read_int() # error_code
|
reader.read_int() # error_code
|
||||||
new_salt = reader.read_long(signed=False)
|
new_salt = reader.read_long(signed=False)
|
||||||
|
@ -306,7 +313,7 @@ class MtProtoSender:
|
||||||
def _handle_bad_msg_notification(self, msg_id, sequence, reader):
|
def _handle_bad_msg_notification(self, msg_id, sequence, reader):
|
||||||
self._logger.debug('Handling bad message notification')
|
self._logger.debug('Handling bad message notification')
|
||||||
reader.read_int(signed=False) # code
|
reader.read_int(signed=False) # code
|
||||||
reader.read_long(signed=False) # request_id
|
reader.read_long() # request_id
|
||||||
reader.read_int() # request_sequence
|
reader.read_int() # request_sequence
|
||||||
|
|
||||||
error_code = reader.read_int()
|
error_code = reader.read_int()
|
||||||
|
@ -316,8 +323,8 @@ class MtProtoSender:
|
||||||
# Use the current msg_id to determine the right time offset.
|
# Use the current msg_id to determine the right time offset.
|
||||||
self.session.update_time_offset(correct_msg_id=msg_id)
|
self.session.update_time_offset(correct_msg_id=msg_id)
|
||||||
self.session.save()
|
self.session.save()
|
||||||
self._logger.warning('Read Bad Message error: ' + str(error))
|
self._logger.debug('Read Bad Message error: ' + str(error))
|
||||||
self._logger.info('Attempting to use the correct time offset.')
|
self._logger.debug('Attempting to use the correct time offset.')
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise error
|
raise error
|
||||||
|
@ -325,7 +332,7 @@ class MtProtoSender:
|
||||||
def _handle_rpc_result(self, msg_id, sequence, reader):
|
def _handle_rpc_result(self, msg_id, sequence, reader):
|
||||||
self._logger.debug('Handling RPC result')
|
self._logger.debug('Handling RPC result')
|
||||||
reader.read_int(signed=False) # code
|
reader.read_int(signed=False) # code
|
||||||
request_id = reader.read_long(signed=False)
|
request_id = reader.read_long()
|
||||||
inner_code = reader.read_int(signed=False)
|
inner_code = reader.read_int(signed=False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -344,7 +351,7 @@ class MtProtoSender:
|
||||||
self._need_confirmation.append(request_id)
|
self._need_confirmation.append(request_id)
|
||||||
self._send_acknowledges()
|
self._send_acknowledges()
|
||||||
|
|
||||||
self._logger.warning('Read RPC error: %s', str(error))
|
self._logger.debug('Read RPC error: %s', str(error))
|
||||||
if isinstance(error, InvalidDCError):
|
if isinstance(error, InvalidDCError):
|
||||||
# Must resend this request, if any
|
# Must resend this request, if any
|
||||||
if request:
|
if request:
|
||||||
|
@ -366,7 +373,7 @@ class MtProtoSender:
|
||||||
else:
|
else:
|
||||||
# If it's really a result for RPC from previous connection
|
# If it's really a result for RPC from previous connection
|
||||||
# session, it will be skipped by the handle_container()
|
# session, it will be skipped by the handle_container()
|
||||||
self._logger.warning('Lost request will be skipped.')
|
self._logger.debug('Lost request will be skipped.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _handle_gzip_packed(self, msg_id, sequence, reader, updates):
|
def _handle_gzip_packed(self, msg_id, sequence, reader, updates):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from binascii import crc32
|
from zlib import crc32
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from ..errors import InvalidChecksumError
|
from ..errors import InvalidChecksumError
|
||||||
|
@ -7,19 +7,26 @@ from ..extensions import BinaryWriter
|
||||||
|
|
||||||
|
|
||||||
class TcpTransport:
|
class TcpTransport:
|
||||||
def __init__(self, ip_address, port, proxy=None):
|
def __init__(self, ip_address, port,
|
||||||
|
proxy=None, timeout=timedelta(seconds=5)):
|
||||||
self.ip = ip_address
|
self.ip = ip_address
|
||||||
self.port = port
|
self.port = port
|
||||||
self.tcp_client = TcpClient(proxy)
|
self.tcp_client = TcpClient(proxy)
|
||||||
|
self.timeout = timeout
|
||||||
self.send_counter = 0
|
self.send_counter = 0
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Connects to the specified IP address and port"""
|
"""Connects to the specified IP address and port"""
|
||||||
self.send_counter = 0
|
self.send_counter = 0
|
||||||
self.tcp_client.connect(self.ip, self.port)
|
self.tcp_client.connect(self.ip, self.port,
|
||||||
|
timeout=round(self.timeout.seconds))
|
||||||
|
|
||||||
|
def is_connected(self):
|
||||||
|
return self.tcp_client.connected
|
||||||
|
|
||||||
# Original reference: https://core.telegram.org/mtproto#tcp-transport
|
# Original reference: https://core.telegram.org/mtproto#tcp-transport
|
||||||
# The packets are encoded as: total length, sequence number, packet and checksum (CRC32)
|
# The packets are encoded as:
|
||||||
|
# total length, sequence number, packet and checksum (CRC32)
|
||||||
def send(self, packet):
|
def send(self, packet):
|
||||||
"""Sends the given packet (bytes array) to the connected peer"""
|
"""Sends the given packet (bytes array) to the connected peer"""
|
||||||
if not self.tcp_client.connected:
|
if not self.tcp_client.connected:
|
||||||
|
@ -36,10 +43,14 @@ class TcpTransport:
|
||||||
self.send_counter += 1
|
self.send_counter += 1
|
||||||
self.tcp_client.write(writer.get_bytes())
|
self.tcp_client.write(writer.get_bytes())
|
||||||
|
|
||||||
def receive(self, timeout=timedelta(seconds=5)):
|
def receive(self, **kwargs):
|
||||||
"""Receives a TCP message (tuple(sequence number, body)) from the connected peer.
|
"""Receives a TCP message (tuple(sequence number, body)) from the
|
||||||
There is a default timeout of 5 seconds before the operation is cancelled.
|
connected peer.
|
||||||
Timeout can be set to None for no timeout"""
|
|
||||||
|
If a named 'timeout' parameter is present, it will override
|
||||||
|
'self.timeout', and this can be a 'timedelta' or 'None'.
|
||||||
|
"""
|
||||||
|
timeout = kwargs.get('timeout', self.timeout)
|
||||||
|
|
||||||
# First read everything we need
|
# First read everything we need
|
||||||
packet_length_bytes = self.tcp_client.read(4, timeout)
|
packet_length_bytes = self.tcp_client.read(4, timeout)
|
||||||
|
|
|
@ -5,22 +5,27 @@ from os import path
|
||||||
|
|
||||||
# Import some externalized utilities to work with the Telegram types and more
|
# Import some externalized utilities to work with the Telegram types and more
|
||||||
from . import helpers as utils
|
from . import helpers as utils
|
||||||
from .errors import RPCError, FloodWaitError
|
from .errors import (
|
||||||
|
RPCError, FloodWaitError, FileMigrateError, TypeNotFoundError
|
||||||
|
)
|
||||||
from .network import authenticator, MtProtoSender, TcpTransport
|
from .network import authenticator, MtProtoSender, TcpTransport
|
||||||
from .utils import get_appropriated_part_size
|
from .utils import get_appropriated_part_size
|
||||||
|
|
||||||
# For sending and receiving requests
|
# For sending and receiving requests
|
||||||
from .tl import MTProtoRequest
|
from .tl import TLObject, JsonSession
|
||||||
from .tl.all_tlobjects import layer
|
from .tl.all_tlobjects import layer
|
||||||
from .tl.functions import (InitConnectionRequest, InvokeWithLayerRequest)
|
from .tl.functions import (InitConnectionRequest, InvokeWithLayerRequest)
|
||||||
|
|
||||||
# Initial request
|
# Initial request
|
||||||
from .tl.functions.help import GetConfigRequest
|
from .tl.functions.help import GetConfigRequest
|
||||||
from .tl.functions.auth import ImportAuthorizationRequest
|
from .tl.functions.auth import (
|
||||||
|
ImportAuthorizationRequest, ExportAuthorizationRequest
|
||||||
|
)
|
||||||
|
|
||||||
# Easier access for working with media
|
# Easier access for working with media
|
||||||
from .tl.functions.upload import (
|
from .tl.functions.upload import (
|
||||||
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest)
|
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest
|
||||||
|
)
|
||||||
|
|
||||||
# All the types we need to work with
|
# All the types we need to work with
|
||||||
from .tl.types import InputFile, InputFileBig
|
from .tl.types import InputFile, InputFileBig
|
||||||
|
@ -47,11 +52,12 @@ class TelegramBareClient:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Current TelegramClient version
|
# Current TelegramClient version
|
||||||
__version__ = '0.11'
|
__version__ = '0.11.5'
|
||||||
|
|
||||||
# region Initialization
|
# region Initialization
|
||||||
|
|
||||||
def __init__(self, session, api_id, api_hash, proxy=None):
|
def __init__(self, session, api_id, api_hash,
|
||||||
|
proxy=None, timeout=timedelta(seconds=5)):
|
||||||
"""Initializes the Telegram client with the specified API ID and Hash.
|
"""Initializes the Telegram client with the specified API ID and Hash.
|
||||||
Session must always be a Session instance, and an optional proxy
|
Session must always be a Session instance, and an optional proxy
|
||||||
can also be specified to be used on the connection.
|
can also be specified to be used on the connection.
|
||||||
|
@ -60,11 +66,17 @@ class TelegramBareClient:
|
||||||
self.api_id = int(api_id)
|
self.api_id = int(api_id)
|
||||||
self.api_hash = api_hash
|
self.api_hash = api_hash
|
||||||
self.proxy = proxy
|
self.proxy = proxy
|
||||||
|
self._timeout = timeout
|
||||||
self._logger = logging.getLogger(__name__)
|
self._logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Cache "exported" senders 'dc_id: TelegramBareClient' and
|
||||||
|
# their corresponding sessions not to recreate them all
|
||||||
|
# the time since it's a (somewhat expensive) process.
|
||||||
|
self._cached_clients = {}
|
||||||
|
|
||||||
# These will be set later
|
# These will be set later
|
||||||
self.dc_options = None
|
self.dc_options = None
|
||||||
self.sender = None
|
self._sender = None
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
@ -79,8 +91,16 @@ class TelegramBareClient:
|
||||||
If 'exported_auth' is not None, it will be used instead to
|
If 'exported_auth' is not None, it will be used instead to
|
||||||
determine the authorization key for the current session.
|
determine the authorization key for the current session.
|
||||||
"""
|
"""
|
||||||
|
if self._sender and self._sender.is_connected():
|
||||||
|
self._logger.debug(
|
||||||
|
'Attempted to connect when the client was already connected.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
transport = TcpTransport(self.session.server_address,
|
transport = TcpTransport(self.session.server_address,
|
||||||
self.session.port, proxy=self.proxy)
|
self.session.port,
|
||||||
|
proxy=self.proxy,
|
||||||
|
timeout=self._timeout)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.session.auth_key:
|
if not self.session.auth_key:
|
||||||
|
@ -89,8 +109,8 @@ class TelegramBareClient:
|
||||||
|
|
||||||
self.session.save()
|
self.session.save()
|
||||||
|
|
||||||
self.sender = MtProtoSender(transport, self.session)
|
self._sender = MtProtoSender(transport, self.session)
|
||||||
self.sender.connect()
|
self._sender.connect()
|
||||||
|
|
||||||
# Now it's time to send an InitConnectionRequest
|
# Now it's time to send an InitConnectionRequest
|
||||||
# This must always be invoked with the layer we'll be using
|
# This must always be invoked with the layer we'll be using
|
||||||
|
@ -106,34 +126,40 @@ class TelegramBareClient:
|
||||||
system_version=self.session.system_version,
|
system_version=self.session.system_version,
|
||||||
app_version=self.session.app_version,
|
app_version=self.session.app_version,
|
||||||
lang_code=self.session.lang_code,
|
lang_code=self.session.lang_code,
|
||||||
|
system_lang_code=self.session.system_lang_code,
|
||||||
|
lang_pack='', # "langPacks are for official apps only"
|
||||||
query=query)
|
query=query)
|
||||||
|
|
||||||
result = self.invoke(
|
result = self(InvokeWithLayerRequest(
|
||||||
InvokeWithLayerRequest(
|
layer=layer, query=request
|
||||||
layer=layer, query=request))
|
))
|
||||||
|
|
||||||
if exported_auth is not None:
|
if exported_auth is not None:
|
||||||
# TODO Don't actually need this for exported authorizations,
|
result = self(GetConfigRequest())
|
||||||
# they're only valid on such data center.
|
|
||||||
result = self.invoke(GetConfigRequest())
|
|
||||||
|
|
||||||
# We're only interested in the DC options,
|
# We're only interested in the DC options,
|
||||||
# although many other options are available!
|
# although many other options are available!
|
||||||
self.dc_options = result.dc_options
|
self.dc_options = result.dc_options
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
except TypeNotFoundError as e:
|
||||||
|
# This is fine, probably layer migration
|
||||||
|
self._logger.debug('Found invalid item, probably migrating', e)
|
||||||
|
self.disconnect()
|
||||||
|
self.connect(exported_auth=exported_auth)
|
||||||
|
|
||||||
except (RPCError, ConnectionError) as error:
|
except (RPCError, ConnectionError) as error:
|
||||||
# Probably errors from the previous session, ignore them
|
# Probably errors from the previous session, ignore them
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
self._logger.warning('Could not stabilise initial connection: {}'
|
self._logger.debug('Could not stabilise initial connection: {}'
|
||||||
.format(error))
|
.format(error))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
"""Disconnects from the Telegram server"""
|
"""Disconnects from the Telegram server"""
|
||||||
if self.sender:
|
if self._sender:
|
||||||
self.sender.disconnect()
|
self._sender.disconnect()
|
||||||
self.sender = None
|
self._sender = None
|
||||||
|
|
||||||
def reconnect(self, new_dc=None):
|
def reconnect(self, new_dc=None):
|
||||||
"""Disconnects and connects again (effectively reconnecting).
|
"""Disconnects and connects again (effectively reconnecting).
|
||||||
|
@ -154,6 +180,30 @@ class TelegramBareClient:
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
# region Properties
|
||||||
|
|
||||||
|
def set_timeout(self, timeout):
|
||||||
|
if timeout is None:
|
||||||
|
self._timeout = None
|
||||||
|
elif isinstance(timeout, int) or isinstance(timeout, float):
|
||||||
|
self._timeout = timedelta(seconds=timeout)
|
||||||
|
elif isinstance(timeout, timedelta):
|
||||||
|
self._timeout = timeout
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'{} is not a valid type for a timeout'.format(type(timeout))
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._sender:
|
||||||
|
self._sender.transport.timeout = self._timeout
|
||||||
|
|
||||||
|
def get_timeout(self):
|
||||||
|
return self._timeout
|
||||||
|
|
||||||
|
timeout = property(get_timeout, set_timeout)
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
# region Working with different Data Centers
|
# region Working with different Data Centers
|
||||||
|
|
||||||
def _get_dc(self, dc_id):
|
def _get_dc(self, dc_id):
|
||||||
|
@ -165,40 +215,88 @@ class TelegramBareClient:
|
||||||
|
|
||||||
return next(dc for dc in self.dc_options if dc.id == dc_id)
|
return next(dc for dc in self.dc_options if dc.id == dc_id)
|
||||||
|
|
||||||
|
def _get_exported_client(self, dc_id,
|
||||||
|
init_connection=False,
|
||||||
|
bypass_cache=False):
|
||||||
|
"""Gets a cached exported TelegramBareClient for the desired DC.
|
||||||
|
|
||||||
|
If it's the first time retrieving the TelegramBareClient, the
|
||||||
|
current authorization is exported to the new DC so that
|
||||||
|
it can be used there, and the connection is initialized.
|
||||||
|
|
||||||
|
If after using the sender a ConnectionResetError is raised,
|
||||||
|
this method should be called again with init_connection=True
|
||||||
|
in order to perform the reconnection.
|
||||||
|
|
||||||
|
If bypass_cache is True, a new client will be exported and
|
||||||
|
it will not be cached.
|
||||||
|
"""
|
||||||
|
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
|
||||||
|
# for clearly showing how to export the authorization! ^^
|
||||||
|
client = self._cached_clients.get(dc_id)
|
||||||
|
if client and not bypass_cache:
|
||||||
|
if init_connection:
|
||||||
|
client.reconnect()
|
||||||
|
return client
|
||||||
|
else:
|
||||||
|
dc = self._get_dc(dc_id)
|
||||||
|
|
||||||
|
# Export the current authorization to the new DC.
|
||||||
|
export_auth = self(ExportAuthorizationRequest(dc_id))
|
||||||
|
|
||||||
|
# Create a temporary session for this IP address, which needs
|
||||||
|
# to be different because each auth_key is unique per DC.
|
||||||
|
#
|
||||||
|
# Construct this session with the connection parameters
|
||||||
|
# (system version, device model...) from the current one.
|
||||||
|
session = JsonSession(self.session)
|
||||||
|
session.server_address = dc.ip_address
|
||||||
|
session.port = dc.port
|
||||||
|
client = TelegramBareClient(
|
||||||
|
session, self.api_id, self.api_hash,
|
||||||
|
timeout=self._timeout
|
||||||
|
)
|
||||||
|
client.connect(exported_auth=export_auth)
|
||||||
|
|
||||||
|
if not bypass_cache:
|
||||||
|
# Don't go through this expensive process every time.
|
||||||
|
self._cached_clients[dc_id] = client
|
||||||
|
return client
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Invoking Telegram requests
|
# region Invoking Telegram requests
|
||||||
|
|
||||||
def invoke(self, request, timeout=timedelta(seconds=5), updates=None):
|
def invoke(self, request, updates=None):
|
||||||
"""Invokes (sends) a MTProtoRequest and returns (receives) its result.
|
"""Invokes (sends) a MTProtoRequest and returns (receives) its result.
|
||||||
|
|
||||||
An optional timeout can be specified to cancel the operation if no
|
|
||||||
result is received within such time, or None to disable any timeout.
|
|
||||||
|
|
||||||
If 'updates' is not None, all read update object will be put
|
If 'updates' is not None, all read update object will be put
|
||||||
in such list. Otherwise, update objects will be ignored.
|
in such list. Otherwise, update objects will be ignored.
|
||||||
"""
|
"""
|
||||||
if not isinstance(request, MTProtoRequest):
|
if not isinstance(request, TLObject) and not request.content_related:
|
||||||
raise ValueError('You can only invoke MtProtoRequests')
|
raise ValueError('You can only invoke requests, not types!')
|
||||||
|
|
||||||
if not self.sender:
|
if not self._sender:
|
||||||
raise ValueError('You must be connected to invoke requests!')
|
raise ValueError('You must be connected to invoke requests!')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.sender.send(request)
|
self._sender.send(request)
|
||||||
self.sender.receive(request, timeout, updates=updates)
|
self._sender.receive(request, updates=updates)
|
||||||
return request.result
|
return request.result
|
||||||
|
|
||||||
except ConnectionResetError:
|
except ConnectionResetError:
|
||||||
self._logger.info('Server disconnected us. Reconnecting and '
|
self._logger.debug('Server disconnected us. Reconnecting and '
|
||||||
'resending request...')
|
'resending request...')
|
||||||
self.reconnect()
|
self.reconnect()
|
||||||
return self.invoke(request, timeout=timeout)
|
return self.invoke(request)
|
||||||
|
|
||||||
except FloodWaitError:
|
except FloodWaitError:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# Let people use client(SomeRequest()) instead client.invoke(...)
|
||||||
|
__call__ = invoke
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Uploading media
|
# region Uploading media
|
||||||
|
@ -250,7 +348,7 @@ class TelegramBareClient:
|
||||||
else:
|
else:
|
||||||
request = SaveFilePartRequest(file_id, part_index, part)
|
request = SaveFilePartRequest(file_id, part_index, part)
|
||||||
|
|
||||||
result = self.invoke(request)
|
result = self(request)
|
||||||
if result:
|
if result:
|
||||||
if not is_large:
|
if not is_large:
|
||||||
# No need to update the hash if it's a large file
|
# No need to update the hash if it's a large file
|
||||||
|
@ -305,12 +403,21 @@ class TelegramBareClient:
|
||||||
else:
|
else:
|
||||||
f = file
|
f = file
|
||||||
|
|
||||||
|
# The used client will change if FileMigrateError occurs
|
||||||
|
client = self
|
||||||
|
|
||||||
try:
|
try:
|
||||||
offset_index = 0
|
offset_index = 0
|
||||||
while True:
|
while True:
|
||||||
offset = offset_index * part_size
|
offset = offset_index * part_size
|
||||||
result = self.invoke(
|
|
||||||
GetFileRequest(input_location, offset, part_size))
|
try:
|
||||||
|
result = client(
|
||||||
|
GetFileRequest(input_location, offset, part_size))
|
||||||
|
except FileMigrateError as e:
|
||||||
|
client = self._get_exported_client(e.new_dc)
|
||||||
|
continue
|
||||||
|
|
||||||
offset_index += 1
|
offset_index += 1
|
||||||
|
|
||||||
# If we have received no data (0 bytes), the file is over
|
# If we have received no data (0 bytes), the file is over
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from threading import Event, RLock, Thread
|
from threading import Event, RLock, Thread
|
||||||
from time import sleep
|
from time import sleep, time
|
||||||
|
|
||||||
from . import TelegramBareClient
|
from . import TelegramBareClient
|
||||||
|
|
||||||
# Import some externalized utilities to work with the Telegram types and more
|
# Import some externalized utilities to work with the Telegram types and more
|
||||||
from . import helpers as utils
|
from . import helpers as utils
|
||||||
from .errors import (RPCError, UnauthorizedError, InvalidParameterError,
|
from .errors import (RPCError, UnauthorizedError, InvalidParameterError,
|
||||||
ReadCancelledError, FileMigrateError, PhoneMigrateError,
|
ReadCancelledError, PhoneCodeEmptyError,
|
||||||
NetworkMigrateError, UserMigrateError, PhoneCodeEmptyError,
|
PhoneMigrateError, NetworkMigrateError, UserMigrateError,
|
||||||
PhoneCodeExpiredError, PhoneCodeHashEmptyError,
|
PhoneCodeExpiredError, PhoneCodeHashEmptyError,
|
||||||
PhoneCodeInvalidError, InvalidChecksumError)
|
PhoneCodeInvalidError, InvalidChecksumError)
|
||||||
|
|
||||||
# For sending and receiving requests
|
# For sending and receiving requests
|
||||||
from .tl import MTProtoRequest, Session, JsonSession
|
from .tl import Session, JsonSession
|
||||||
|
|
||||||
# Required to get the password salt
|
# Required to get the password salt
|
||||||
from .tl.functions.account import GetPasswordRequest
|
from .tl.functions.account import GetPasswordRequest
|
||||||
|
@ -24,9 +24,6 @@ from .tl.functions.auth import (CheckPasswordRequest, LogOutRequest,
|
||||||
SendCodeRequest, SignInRequest,
|
SendCodeRequest, SignInRequest,
|
||||||
SignUpRequest, ImportBotAuthorizationRequest)
|
SignUpRequest, ImportBotAuthorizationRequest)
|
||||||
|
|
||||||
# Required to work with different data centers
|
|
||||||
from .tl.functions.auth import ExportAuthorizationRequest
|
|
||||||
|
|
||||||
# Easier access to common methods
|
# Easier access to common methods
|
||||||
from .tl.functions.messages import (
|
from .tl.functions.messages import (
|
||||||
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
|
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
|
||||||
|
@ -35,6 +32,9 @@ from .tl.functions.messages import (
|
||||||
# For .get_me() and ensuring we're authorized
|
# For .get_me() and ensuring we're authorized
|
||||||
from .tl.functions.users import GetUsersRequest
|
from .tl.functions.users import GetUsersRequest
|
||||||
|
|
||||||
|
# So the server doesn't stop sending updates to us
|
||||||
|
from .tl.functions import PingRequest
|
||||||
|
|
||||||
# All the types we need to work with
|
# All the types we need to work with
|
||||||
from .tl.types import (
|
from .tl.types import (
|
||||||
ChatPhotoEmpty, DocumentAttributeAudio, DocumentAttributeFilename,
|
ChatPhotoEmpty, DocumentAttributeAudio, DocumentAttributeFilename,
|
||||||
|
@ -61,7 +61,9 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
def __init__(self, session, api_id, api_hash, proxy=None,
|
def __init__(self, session, api_id, api_hash, proxy=None,
|
||||||
device_model=None, system_version=None,
|
device_model=None, system_version=None,
|
||||||
app_version=None, lang_code=None):
|
app_version=None, lang_code=None,
|
||||||
|
system_lang_code=None,
|
||||||
|
timeout=timedelta(seconds=5)):
|
||||||
"""Initializes the Telegram client with the specified API ID and Hash.
|
"""Initializes the Telegram client with the specified API ID and Hash.
|
||||||
|
|
||||||
Session can either be a `str` object (filename for the .session)
|
Session can either be a `str` object (filename for the .session)
|
||||||
|
@ -70,12 +72,12 @@ class TelegramClient(TelegramBareClient):
|
||||||
session - remember to '.log_out()'!
|
session - remember to '.log_out()'!
|
||||||
|
|
||||||
Default values for the optional parameters if left as None are:
|
Default values for the optional parameters if left as None are:
|
||||||
device_model = platform.node()
|
device_model = platform.node()
|
||||||
system_version = platform.system()
|
system_version = platform.system()
|
||||||
app_version = TelegramClient.__version__
|
app_version = TelegramClient.__version__
|
||||||
lang_code = 'en'
|
lang_code = 'en'
|
||||||
|
system_lang_code = lang_code
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not api_id or not api_hash:
|
if not api_id or not api_hash:
|
||||||
raise PermissionError(
|
raise PermissionError(
|
||||||
"Your API ID or Hash cannot be empty or None. "
|
"Your API ID or Hash cannot be empty or None. "
|
||||||
|
@ -85,20 +87,23 @@ class TelegramClient(TelegramBareClient):
|
||||||
# TODO JsonSession until migration is complete (by v1.0)
|
# TODO JsonSession until migration is complete (by v1.0)
|
||||||
if isinstance(session, str) or session is None:
|
if isinstance(session, str) or session is None:
|
||||||
session = JsonSession.try_load_or_create_new(session)
|
session = JsonSession.try_load_or_create_new(session)
|
||||||
elif not isinstance(session, Session):
|
elif not isinstance(session, Session) and not isinstance(session, JsonSession):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'The given session must be a str or a Session instance.')
|
'The given session must be a str or a Session instance.')
|
||||||
|
|
||||||
super().__init__(session, api_id, api_hash, proxy)
|
super().__init__(session, api_id, api_hash, proxy, timeout=timeout)
|
||||||
|
|
||||||
# Safety across multiple threads (for the updates thread)
|
# Safety across multiple threads (for the updates thread)
|
||||||
self._lock = RLock()
|
self._lock = RLock()
|
||||||
|
|
||||||
# Methods to be called when an update is received
|
# Updates-related members
|
||||||
self._update_handlers = []
|
self._update_handlers = []
|
||||||
self._updates_thread_running = Event()
|
self._updates_thread_running = Event()
|
||||||
self._updates_thread_receiving = Event()
|
self._updates_thread_receiving = Event()
|
||||||
|
|
||||||
|
self._next_ping_at = 0
|
||||||
|
self.ping_interval = 60 # Seconds
|
||||||
|
|
||||||
# Used on connection - the user may modify these and reconnect
|
# Used on connection - the user may modify these and reconnect
|
||||||
if device_model:
|
if device_model:
|
||||||
self.session.device_model = device_model
|
self.session.device_model = device_model
|
||||||
|
@ -112,10 +117,9 @@ class TelegramClient(TelegramBareClient):
|
||||||
if lang_code:
|
if lang_code:
|
||||||
self.session.lang_code = lang_code
|
self.session.lang_code = lang_code
|
||||||
|
|
||||||
# Cache "exported" senders 'dc_id: MtProtoSender' and
|
self.session.system_lang_code = \
|
||||||
# their corresponding sessions not to recreate them all
|
system_lang_code if system_lang_code else self.session.lang_code
|
||||||
# the time since it's a (somewhat expensive) process.
|
|
||||||
self._cached_clients = {}
|
|
||||||
self._updates_thread = None
|
self._updates_thread = None
|
||||||
self._phone_code_hashes = {}
|
self._phone_code_hashes = {}
|
||||||
|
|
||||||
|
@ -129,15 +133,17 @@ class TelegramClient(TelegramBareClient):
|
||||||
not the same as authenticating the desired user itself, which
|
not the same as authenticating the desired user itself, which
|
||||||
may require a call (or several) to 'sign_in' for the first time.
|
may require a call (or several) to 'sign_in' for the first time.
|
||||||
|
|
||||||
|
The specified timeout will be used on internal .invoke()'s.
|
||||||
|
|
||||||
*args will be ignored.
|
*args will be ignored.
|
||||||
"""
|
"""
|
||||||
return super(TelegramClient, self).connect()
|
return super().connect()
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
"""Disconnects from the Telegram server
|
"""Disconnects from the Telegram server
|
||||||
and stops all the spawned threads"""
|
and stops all the spawned threads"""
|
||||||
self._set_updates_thread(running=False)
|
self._set_updates_thread(running=False)
|
||||||
super(TelegramClient, self).disconnect()
|
super().disconnect()
|
||||||
|
|
||||||
# Also disconnect all the cached senders
|
# Also disconnect all the cached senders
|
||||||
for sender in self._cached_clients.values():
|
for sender in self._cached_clients.values():
|
||||||
|
@ -149,52 +155,6 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
# region Working with different connections
|
# region Working with different connections
|
||||||
|
|
||||||
def _get_exported_client(self, dc_id,
|
|
||||||
init_connection=False,
|
|
||||||
bypass_cache=False):
|
|
||||||
"""Gets a cached exported TelegramBareClient for the desired DC.
|
|
||||||
|
|
||||||
If it's the first time retrieving the TelegramBareClient, the
|
|
||||||
current authorization is exported to the new DC so that
|
|
||||||
it can be used there, and the connection is initialized.
|
|
||||||
|
|
||||||
If after using the sender a ConnectionResetError is raised,
|
|
||||||
this method should be called again with init_connection=True
|
|
||||||
in order to perform the reconnection.
|
|
||||||
|
|
||||||
If bypass_cache is True, a new client will be exported and
|
|
||||||
it will not be cached.
|
|
||||||
"""
|
|
||||||
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
|
|
||||||
# for clearly showing how to export the authorization! ^^
|
|
||||||
|
|
||||||
client = self._cached_clients.get(dc_id)
|
|
||||||
if client and not bypass_cache:
|
|
||||||
if init_connection:
|
|
||||||
client.reconnect()
|
|
||||||
return client
|
|
||||||
else:
|
|
||||||
dc = self._get_dc(dc_id)
|
|
||||||
|
|
||||||
# Export the current authorization to the new DC.
|
|
||||||
export_auth = self.invoke(ExportAuthorizationRequest(dc_id))
|
|
||||||
|
|
||||||
# Create a temporary session for this IP address, which needs
|
|
||||||
# to be different because each auth_key is unique per DC.
|
|
||||||
#
|
|
||||||
# Construct this session with the connection parameters
|
|
||||||
# (system version, device model...) from the current one.
|
|
||||||
session = JsonSession(self.session)
|
|
||||||
session.server_address = dc.ip_address
|
|
||||||
session.port = dc.port
|
|
||||||
client = TelegramBareClient(session, self.api_id, self.api_hash)
|
|
||||||
client.connect(exported_auth=export_auth)
|
|
||||||
|
|
||||||
if not bypass_cache:
|
|
||||||
# Don't go through this expensive process every time.
|
|
||||||
self._cached_clients[dc_id] = client
|
|
||||||
return client
|
|
||||||
|
|
||||||
def create_new_connection(self, on_dc=None):
|
def create_new_connection(self, on_dc=None):
|
||||||
"""Creates a new connection which can be used in parallel
|
"""Creates a new connection which can be used in parallel
|
||||||
with the original TelegramClient. A TelegramBareClient
|
with the original TelegramClient. A TelegramBareClient
|
||||||
|
@ -206,15 +166,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
If the client is meant to be used on a different data
|
If the client is meant to be used on a different data
|
||||||
center, the data center ID should be specified instead.
|
center, the data center ID should be specified instead.
|
||||||
|
|
||||||
Note that TelegramBareClients will not handle automatic
|
|
||||||
reconnection (i.e. switching to another data center to
|
|
||||||
download media), and InvalidDCError will be raised in
|
|
||||||
such case.
|
|
||||||
"""
|
"""
|
||||||
if on_dc is None:
|
if on_dc is None:
|
||||||
client = TelegramBareClient(self.session, self.api_id, self.api_hash,
|
client = TelegramBareClient(
|
||||||
proxy=self.proxy)
|
self.session, self.api_id, self.api_hash, proxy=self.proxy)
|
||||||
client.connect()
|
client.connect()
|
||||||
else:
|
else:
|
||||||
client = self._get_exported_client(on_dc, bypass_cache=True)
|
client = self._get_exported_client(on_dc, bypass_cache=True)
|
||||||
|
@ -225,7 +180,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
# region Telegram requests functions
|
# region Telegram requests functions
|
||||||
|
|
||||||
def invoke(self, request, timeout=timedelta(seconds=5), *args):
|
def invoke(self, request, *args):
|
||||||
"""Invokes (sends) a MTProtoRequest and returns (receives) its result.
|
"""Invokes (sends) a MTProtoRequest and returns (receives) its result.
|
||||||
|
|
||||||
An optional timeout can be specified to cancel the operation if no
|
An optional timeout can be specified to cancel the operation if no
|
||||||
|
@ -233,21 +188,16 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
*args will be ignored.
|
*args will be ignored.
|
||||||
"""
|
"""
|
||||||
if not issubclass(type(request), MTProtoRequest):
|
|
||||||
raise ValueError('You can only invoke MtProtoRequests')
|
|
||||||
|
|
||||||
if not self.sender:
|
|
||||||
raise ValueError('You must be connected to invoke requests!')
|
|
||||||
|
|
||||||
if self._updates_thread_receiving.is_set():
|
if self._updates_thread_receiving.is_set():
|
||||||
self.sender.cancel_receive()
|
self._sender.cancel_receive()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._lock.acquire()
|
self._lock.acquire()
|
||||||
|
|
||||||
updates = [] if self._update_handlers else None
|
updates = [] if self._update_handlers else None
|
||||||
result = super(TelegramClient, self).invoke(
|
result = super().invoke(
|
||||||
request, timeout=timeout, updates=updates)
|
request, updates=updates
|
||||||
|
)
|
||||||
|
|
||||||
if updates:
|
if updates:
|
||||||
for update in updates:
|
for update in updates:
|
||||||
|
@ -258,18 +208,20 @@ class TelegramClient(TelegramBareClient):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except (PhoneMigrateError, NetworkMigrateError, UserMigrateError) as e:
|
except (PhoneMigrateError, NetworkMigrateError, UserMigrateError) as e:
|
||||||
self._logger.info('DC error when invoking request, '
|
self._logger.debug('DC error when invoking request, '
|
||||||
'attempting to reconnect at DC {}'
|
'attempting to reconnect at DC {}'
|
||||||
.format(e.new_dc))
|
.format(e.new_dc))
|
||||||
|
|
||||||
self.reconnect(new_dc=e.new_dc)
|
self.reconnect(new_dc=e.new_dc)
|
||||||
return self.invoke(request, timeout=timeout)
|
return self.invoke(request)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
|
||||||
def invoke_on_dc(self, request, dc_id,
|
# Let people use client(SomeRequest()) instead client.invoke(...)
|
||||||
timeout=timedelta(seconds=5), reconnect=False):
|
__call__ = invoke
|
||||||
|
|
||||||
|
def invoke_on_dc(self, request, dc_id, reconnect=False):
|
||||||
"""Invokes the given request on a different DC
|
"""Invokes the given request on a different DC
|
||||||
by making use of the exported MtProtoSenders.
|
by making use of the exported MtProtoSenders.
|
||||||
|
|
||||||
|
@ -286,8 +238,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
if reconnect:
|
if reconnect:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
return self.invoke_on_dc(request, dc_id,
|
return self.invoke_on_dc(request, dc_id, reconnect=True)
|
||||||
timeout=timeout, reconnect=True)
|
|
||||||
|
|
||||||
# region Authorization requests
|
# region Authorization requests
|
||||||
|
|
||||||
|
@ -298,7 +249,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
def send_code_request(self, phone_number):
|
def send_code_request(self, phone_number):
|
||||||
"""Sends a code request to the specified phone number"""
|
"""Sends a code request to the specified phone number"""
|
||||||
result = self.invoke(
|
result = self(
|
||||||
SendCodeRequest(phone_number, self.api_id, self.api_hash))
|
SendCodeRequest(phone_number, self.api_id, self.api_hash))
|
||||||
|
|
||||||
self._phone_code_hashes[phone_number] = result.phone_code_hash
|
self._phone_code_hashes[phone_number] = result.phone_code_hash
|
||||||
|
@ -324,7 +275,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
'Please make sure to call send_code_request first.')
|
'Please make sure to call send_code_request first.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.invoke(SignInRequest(
|
result = self(SignInRequest(
|
||||||
phone_number, self._phone_code_hashes[phone_number], code))
|
phone_number, self._phone_code_hashes[phone_number], code))
|
||||||
|
|
||||||
except (PhoneCodeEmptyError, PhoneCodeExpiredError,
|
except (PhoneCodeEmptyError, PhoneCodeExpiredError,
|
||||||
|
@ -332,12 +283,12 @@ class TelegramClient(TelegramBareClient):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
elif password:
|
elif password:
|
||||||
salt = self.invoke(GetPasswordRequest()).current_salt
|
salt = self(GetPasswordRequest()).current_salt
|
||||||
result = self.invoke(
|
result = self(
|
||||||
CheckPasswordRequest(utils.get_password_hash(password, salt)))
|
CheckPasswordRequest(utils.get_password_hash(password, salt)))
|
||||||
|
|
||||||
elif bot_token:
|
elif bot_token:
|
||||||
result = self.invoke(ImportBotAuthorizationRequest(
|
result = self(ImportBotAuthorizationRequest(
|
||||||
flags=0, bot_auth_token=bot_token,
|
flags=0, bot_auth_token=bot_token,
|
||||||
api_id=self.api_id, api_hash=self.api_hash))
|
api_id=self.api_id, api_hash=self.api_hash))
|
||||||
|
|
||||||
|
@ -350,7 +301,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
def sign_up(self, phone_number, code, first_name, last_name=''):
|
def sign_up(self, phone_number, code, first_name, last_name=''):
|
||||||
"""Signs up to Telegram. Make sure you sent a code request first!"""
|
"""Signs up to Telegram. Make sure you sent a code request first!"""
|
||||||
result = self.invoke(
|
result = self(
|
||||||
SignUpRequest(
|
SignUpRequest(
|
||||||
phone_number=phone_number,
|
phone_number=phone_number,
|
||||||
phone_code_hash=self._phone_code_hashes[phone_number],
|
phone_code_hash=self._phone_code_hashes[phone_number],
|
||||||
|
@ -366,9 +317,9 @@ class TelegramClient(TelegramBareClient):
|
||||||
Returns True if everything went okay."""
|
Returns True if everything went okay."""
|
||||||
|
|
||||||
# Special flag when logging out (so the ack request confirms it)
|
# Special flag when logging out (so the ack request confirms it)
|
||||||
self.sender.logging_out = True
|
self._sender.logging_out = True
|
||||||
try:
|
try:
|
||||||
self.invoke(LogOutRequest())
|
self(LogOutRequest())
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
if not self.session.delete():
|
if not self.session.delete():
|
||||||
return False
|
return False
|
||||||
|
@ -377,14 +328,14 @@ class TelegramClient(TelegramBareClient):
|
||||||
return True
|
return True
|
||||||
except (RPCError, ConnectionError):
|
except (RPCError, ConnectionError):
|
||||||
# Something happened when logging out, restore the state back
|
# Something happened when logging out, restore the state back
|
||||||
self.sender.logging_out = False
|
self._sender.logging_out = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_me(self):
|
def get_me(self):
|
||||||
"""Gets "me" (the self user) which is currently authenticated,
|
"""Gets "me" (the self user) which is currently authenticated,
|
||||||
or None if the request fails (hence, not authenticated)."""
|
or None if the request fails (hence, not authenticated)."""
|
||||||
try:
|
try:
|
||||||
return self.invoke(GetUsersRequest([InputUserSelf()]))[0]
|
return self(GetUsersRequest([InputUserSelf()]))[0]
|
||||||
except UnauthorizedError:
|
except UnauthorizedError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -405,7 +356,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
corresponding to that dialog.
|
corresponding to that dialog.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
r = self.invoke(
|
r = self(
|
||||||
GetDialogsRequest(
|
GetDialogsRequest(
|
||||||
offset_date=offset_date,
|
offset_date=offset_date,
|
||||||
offset_id=offset_id,
|
offset_id=offset_id,
|
||||||
|
@ -422,16 +373,18 @@ class TelegramClient(TelegramBareClient):
|
||||||
def send_message(self,
|
def send_message(self,
|
||||||
entity,
|
entity,
|
||||||
message,
|
message,
|
||||||
no_web_page=False):
|
link_preview=True):
|
||||||
"""Sends a message to the given entity (or input peer)
|
"""Sends a message to the given entity (or input peer)
|
||||||
and returns the sent message ID"""
|
and returns the sent message ID"""
|
||||||
request = SendMessageRequest(
|
request = SendMessageRequest(
|
||||||
peer=get_input_peer(entity),
|
peer=get_input_peer(entity),
|
||||||
message=message,
|
message=message,
|
||||||
entities=[],
|
entities=[],
|
||||||
no_webpage=no_web_page
|
no_webpage=not link_preview
|
||||||
)
|
)
|
||||||
self.invoke(request)
|
result = self(request)
|
||||||
|
for handler in self._update_handlers:
|
||||||
|
handler(result)
|
||||||
return request.random_id
|
return request.random_id
|
||||||
|
|
||||||
def get_message_history(self,
|
def get_message_history(self,
|
||||||
|
@ -456,15 +409,15 @@ class TelegramClient(TelegramBareClient):
|
||||||
:return: A tuple containing total message count and two more lists ([messages], [senders]).
|
:return: A tuple containing total message count and two more lists ([messages], [senders]).
|
||||||
Note that the sender can be null if it was not found!
|
Note that the sender can be null if it was not found!
|
||||||
"""
|
"""
|
||||||
result = self.invoke(
|
result = self(GetHistoryRequest(
|
||||||
GetHistoryRequest(
|
get_input_peer(entity),
|
||||||
get_input_peer(entity),
|
limit=limit,
|
||||||
limit=limit,
|
offset_date=offset_date,
|
||||||
offset_date=offset_date,
|
offset_id=offset_id,
|
||||||
offset_id=offset_id,
|
max_id=max_id,
|
||||||
max_id=max_id,
|
min_id=min_id,
|
||||||
min_id=min_id,
|
add_offset=add_offset
|
||||||
add_offset=add_offset))
|
))
|
||||||
|
|
||||||
# The result may be a messages slice (not all messages were retrieved)
|
# The result may be a messages slice (not all messages were retrieved)
|
||||||
# or simply a messages TLObject. In the later case, no "count"
|
# or simply a messages TLObject. In the later case, no "count"
|
||||||
|
@ -498,7 +451,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
else:
|
else:
|
||||||
max_id = messages.id
|
max_id = messages.id
|
||||||
|
|
||||||
return self.invoke(ReadHistoryRequest(peer=get_input_peer(entity), max_id=max_id))
|
return self(ReadHistoryRequest(
|
||||||
|
peer=get_input_peer(entity),
|
||||||
|
max_id=max_id
|
||||||
|
))
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
@ -537,7 +493,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
def send_media_file(self, input_media, entity):
|
def send_media_file(self, input_media, entity):
|
||||||
"""Sends any input_media (contact, document, photo...) to the given entity"""
|
"""Sends any input_media (contact, document, photo...) to the given entity"""
|
||||||
self.invoke(SendMediaRequest(
|
self(SendMediaRequest(
|
||||||
peer=get_input_peer(entity),
|
peer=get_input_peer(entity),
|
||||||
media=input_media
|
media=input_media
|
||||||
))
|
))
|
||||||
|
@ -580,36 +536,41 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
def download_msg_media(self,
|
def download_msg_media(self,
|
||||||
message_media,
|
message_media,
|
||||||
file_path,
|
file,
|
||||||
add_extension=True,
|
add_extension=True,
|
||||||
progress_callback=None):
|
progress_callback=None):
|
||||||
"""Downloads the given MessageMedia (Photo, Document or Contact)
|
"""Downloads the given MessageMedia (Photo, Document or Contact)
|
||||||
into the desired file_path, optionally finding its extension automatically
|
into the desired file (a stream or str), optionally finding its
|
||||||
The progress_callback should be a callback function which takes two parameters,
|
extension automatically.
|
||||||
uploaded size (in bytes) and total file size (in bytes).
|
|
||||||
This will be called every time a part is downloaded"""
|
The progress_callback should be a callback function which takes
|
||||||
|
two parameters, uploaded size and total file size (both in bytes).
|
||||||
|
This will be called every time a part is downloaded
|
||||||
|
"""
|
||||||
if type(message_media) == MessageMediaPhoto:
|
if type(message_media) == MessageMediaPhoto:
|
||||||
return self.download_photo(message_media, file_path, add_extension,
|
return self.download_photo(message_media, file, add_extension,
|
||||||
progress_callback)
|
progress_callback)
|
||||||
|
|
||||||
elif type(message_media) == MessageMediaDocument:
|
elif type(message_media) == MessageMediaDocument:
|
||||||
return self.download_document(message_media, file_path,
|
return self.download_document(message_media, file,
|
||||||
add_extension, progress_callback)
|
add_extension, progress_callback)
|
||||||
|
|
||||||
elif type(message_media) == MessageMediaContact:
|
elif type(message_media) == MessageMediaContact:
|
||||||
return self.download_contact(message_media, file_path,
|
return self.download_contact(message_media, file,
|
||||||
add_extension)
|
add_extension)
|
||||||
|
|
||||||
def download_photo(self,
|
def download_photo(self,
|
||||||
message_media_photo,
|
message_media_photo,
|
||||||
file_path,
|
file,
|
||||||
add_extension=False,
|
add_extension=False,
|
||||||
progress_callback=None):
|
progress_callback=None):
|
||||||
"""Downloads MessageMediaPhoto's largest size into the desired
|
"""Downloads MessageMediaPhoto's largest size into the desired file
|
||||||
file_path, optionally finding its extension automatically
|
(a stream or str), optionally finding its extension automatically.
|
||||||
The progress_callback should be a callback function which takes two parameters,
|
|
||||||
uploaded size (in bytes) and total file size (in bytes).
|
The progress_callback should be a callback function which takes
|
||||||
This will be called every time a part is downloaded"""
|
two parameters, uploaded size and total file size (both in bytes).
|
||||||
|
This will be called every time a part is downloaded
|
||||||
|
"""
|
||||||
|
|
||||||
# Determine the photo and its largest size
|
# Determine the photo and its largest size
|
||||||
photo = message_media_photo.photo
|
photo = message_media_photo.photo
|
||||||
|
@ -617,8 +578,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
file_size = largest_size.size
|
file_size = largest_size.size
|
||||||
largest_size = largest_size.location
|
largest_size = largest_size.location
|
||||||
|
|
||||||
if add_extension:
|
if isinstance(file, str) and add_extension:
|
||||||
file_path += get_extension(message_media_photo)
|
file += get_extension(message_media_photo)
|
||||||
|
|
||||||
# Download the media with the largest size input file location
|
# Download the media with the largest size input file location
|
||||||
self.download_file(
|
self.download_file(
|
||||||
|
@ -627,42 +588,45 @@ class TelegramClient(TelegramBareClient):
|
||||||
local_id=largest_size.local_id,
|
local_id=largest_size.local_id,
|
||||||
secret=largest_size.secret
|
secret=largest_size.secret
|
||||||
),
|
),
|
||||||
file_path,
|
file,
|
||||||
file_size=file_size,
|
file_size=file_size,
|
||||||
progress_callback=progress_callback
|
progress_callback=progress_callback
|
||||||
)
|
)
|
||||||
return file_path
|
return file
|
||||||
|
|
||||||
def download_document(self,
|
def download_document(self,
|
||||||
message_media_document,
|
message_media_document,
|
||||||
file_path=None,
|
file=None,
|
||||||
add_extension=True,
|
add_extension=True,
|
||||||
progress_callback=None):
|
progress_callback=None):
|
||||||
"""Downloads the given MessageMediaDocument into the desired
|
"""Downloads the given MessageMediaDocument into the desired file
|
||||||
file_path, optionally finding its extension automatically.
|
(a stream or str), optionally finding its extension automatically.
|
||||||
If no file_path is given, it will try to be guessed from the document
|
|
||||||
The progress_callback should be a callback function which takes two parameters,
|
If no file_path is given it will try to be guessed from the document.
|
||||||
uploaded size (in bytes) and total file size (in bytes).
|
|
||||||
This will be called every time a part is downloaded"""
|
The progress_callback should be a callback function which takes
|
||||||
|
two parameters, uploaded size and total file size (both in bytes).
|
||||||
|
This will be called every time a part is downloaded
|
||||||
|
"""
|
||||||
document = message_media_document.document
|
document = message_media_document.document
|
||||||
file_size = document.size
|
file_size = document.size
|
||||||
|
|
||||||
# If no file path was given, try to guess it from the attributes
|
# If no file path was given, try to guess it from the attributes
|
||||||
if file_path is None:
|
if file is None:
|
||||||
for attr in document.attributes:
|
for attr in document.attributes:
|
||||||
if type(attr) == DocumentAttributeFilename:
|
if type(attr) == DocumentAttributeFilename:
|
||||||
file_path = attr.file_name
|
file = attr.file_name
|
||||||
break # This attribute has higher preference
|
break # This attribute has higher preference
|
||||||
|
|
||||||
elif type(attr) == DocumentAttributeAudio:
|
elif type(attr) == DocumentAttributeAudio:
|
||||||
file_path = '{} - {}'.format(attr.performer, attr.title)
|
file = '{} - {}'.format(attr.performer, attr.title)
|
||||||
|
|
||||||
if file_path is None:
|
if file is None:
|
||||||
raise ValueError('Could not infer a file_path for the document'
|
raise ValueError('Could not infer a file_path for the document'
|
||||||
'. Please provide a valid file_path manually')
|
'. Please provide a valid file_path manually')
|
||||||
|
|
||||||
if add_extension:
|
if isinstance(file, str) and add_extension:
|
||||||
file_path += get_extension(message_media_document)
|
file += get_extension(message_media_document)
|
||||||
|
|
||||||
self.download_file(
|
self.download_file(
|
||||||
InputDocumentFileLocation(
|
InputDocumentFileLocation(
|
||||||
|
@ -670,74 +634,48 @@ class TelegramClient(TelegramBareClient):
|
||||||
access_hash=document.access_hash,
|
access_hash=document.access_hash,
|
||||||
version=document.version
|
version=document.version
|
||||||
),
|
),
|
||||||
file_path,
|
file,
|
||||||
file_size=file_size,
|
file_size=file_size,
|
||||||
progress_callback=progress_callback
|
progress_callback=progress_callback
|
||||||
)
|
)
|
||||||
return file_path
|
return file
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def download_contact(message_media_contact, file_path, add_extension=True):
|
def download_contact(message_media_contact, file, add_extension=True):
|
||||||
"""Downloads a media contact using the vCard 4.0 format"""
|
"""Downloads a media contact using the vCard 4.0 format"""
|
||||||
|
|
||||||
first_name = message_media_contact.first_name
|
first_name = message_media_contact.first_name
|
||||||
last_name = message_media_contact.last_name
|
last_name = message_media_contact.last_name
|
||||||
phone_number = message_media_contact.phone_number
|
phone_number = message_media_contact.phone_number
|
||||||
|
|
||||||
# The only way we can save a contact in an understandable
|
if isinstance(file, str):
|
||||||
# way by phones is by using the .vCard format
|
# The only way we can save a contact in an understandable
|
||||||
if add_extension:
|
# way by phones is by using the .vCard format
|
||||||
file_path += '.vcard'
|
if add_extension:
|
||||||
|
file += '.vcard'
|
||||||
|
|
||||||
# Ensure that we'll be able to download the contact
|
# Ensure that we'll be able to download the contact
|
||||||
utils.ensure_parent_dir_exists(file_path)
|
utils.ensure_parent_dir_exists(file)
|
||||||
|
f = open(file, 'w', encoding='utf-8')
|
||||||
|
else:
|
||||||
|
f = file
|
||||||
|
|
||||||
with open(file_path, 'w', encoding='utf-8') as file:
|
try:
|
||||||
file.write('BEGIN:VCARD\n')
|
f.write('BEGIN:VCARD\n')
|
||||||
file.write('VERSION:4.0\n')
|
f.write('VERSION:4.0\n')
|
||||||
file.write('N:{};{};;;\n'.format(first_name, last_name
|
f.write('N:{};{};;;\n'.format(
|
||||||
if last_name else ''))
|
first_name, last_name if last_name else '')
|
||||||
file.write('FN:{}\n'.format(' '.join((first_name, last_name))))
|
|
||||||
file.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(
|
|
||||||
phone_number))
|
|
||||||
file.write('END:VCARD\n')
|
|
||||||
|
|
||||||
return file_path
|
|
||||||
|
|
||||||
def download_file(self,
|
|
||||||
input_location,
|
|
||||||
file,
|
|
||||||
part_size_kb=None,
|
|
||||||
file_size=None,
|
|
||||||
progress_callback=None,
|
|
||||||
on_dc=None):
|
|
||||||
"""Downloads the given InputFileLocation to file (a stream or str).
|
|
||||||
|
|
||||||
If 'progress_callback' is not None, it should be a function that
|
|
||||||
takes two parameters, (bytes_downloaded, total_bytes). Note that
|
|
||||||
'total_bytes' simply equals 'file_size', and may be None.
|
|
||||||
"""
|
|
||||||
if on_dc is None:
|
|
||||||
try:
|
|
||||||
super(TelegramClient, self).download_file(
|
|
||||||
input_location,
|
|
||||||
file,
|
|
||||||
part_size_kb=part_size_kb,
|
|
||||||
file_size=file_size,
|
|
||||||
progress_callback=progress_callback
|
|
||||||
)
|
|
||||||
except FileMigrateError as e:
|
|
||||||
on_dc = e.new_dc
|
|
||||||
|
|
||||||
if on_dc is not None:
|
|
||||||
client = self._get_exported_client(on_dc)
|
|
||||||
client.download_file(
|
|
||||||
input_location,
|
|
||||||
file,
|
|
||||||
part_size_kb=part_size_kb,
|
|
||||||
file_size=file_size,
|
|
||||||
progress_callback=progress_callback
|
|
||||||
)
|
)
|
||||||
|
f.write('FN:{}\n'.format(' '.join((first_name, last_name))))
|
||||||
|
f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(
|
||||||
|
phone_number))
|
||||||
|
f.write('END:VCARD\n')
|
||||||
|
finally:
|
||||||
|
# Only close the stream if we opened it
|
||||||
|
if isinstance(file, str):
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return file
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
@ -748,7 +686,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
def add_update_handler(self, handler):
|
def add_update_handler(self, handler):
|
||||||
"""Adds an update handler (a function which takes a TLObject,
|
"""Adds an update handler (a function which takes a TLObject,
|
||||||
an update, as its parameter) and listens for updates"""
|
an update, as its parameter) and listens for updates"""
|
||||||
if not self.sender:
|
if not self._sender:
|
||||||
raise RuntimeError("You can't add update handlers until you've "
|
raise RuntimeError("You can't add update handlers until you've "
|
||||||
"successfully connected to the server.")
|
"successfully connected to the server.")
|
||||||
|
|
||||||
|
@ -771,7 +709,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Different state, update the saved value and behave as required
|
# Different state, update the saved value and behave as required
|
||||||
self._logger.info('Changing updates thread running status to %s', running)
|
self._logger.debug('Changing updates thread running status to %s', running)
|
||||||
if running:
|
if running:
|
||||||
self._updates_thread_running.set()
|
self._updates_thread_running.set()
|
||||||
if not self._updates_thread:
|
if not self._updates_thread:
|
||||||
|
@ -783,7 +721,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
else:
|
else:
|
||||||
self._updates_thread_running.clear()
|
self._updates_thread_running.clear()
|
||||||
if self._updates_thread_receiving.is_set():
|
if self._updates_thread_receiving.is_set():
|
||||||
self.sender.cancel_receive()
|
self._sender.cancel_receive()
|
||||||
|
|
||||||
def _updates_thread_method(self):
|
def _updates_thread_method(self):
|
||||||
"""This method will run until specified and listen for incoming updates"""
|
"""This method will run until specified and listen for incoming updates"""
|
||||||
|
@ -805,10 +743,14 @@ class TelegramClient(TelegramBareClient):
|
||||||
'Trying to receive updates from the updates thread'
|
'Trying to receive updates from the updates thread'
|
||||||
)
|
)
|
||||||
|
|
||||||
updates = self.sender.receive_updates(timeout=timeout)
|
if time() > self._next_ping_at:
|
||||||
|
self._next_ping_at = time() + self.ping_interval
|
||||||
|
self(PingRequest(utils.generate_random_long()))
|
||||||
|
|
||||||
|
updates = self._sender.receive_updates(timeout=timeout)
|
||||||
|
|
||||||
self._updates_thread_receiving.clear()
|
self._updates_thread_receiving.clear()
|
||||||
self._logger.info(
|
self._logger.debug(
|
||||||
'Received {} update(s) from the updates thread'
|
'Received {} update(s) from the updates thread'
|
||||||
.format(len(updates))
|
.format(len(updates))
|
||||||
)
|
)
|
||||||
|
@ -817,28 +759,28 @@ class TelegramClient(TelegramBareClient):
|
||||||
handler(update)
|
handler(update)
|
||||||
|
|
||||||
except ConnectionResetError:
|
except ConnectionResetError:
|
||||||
self._logger.info('Server disconnected us. Reconnecting...')
|
self._logger.debug('Server disconnected us. Reconnecting...')
|
||||||
self.reconnect()
|
self.reconnect()
|
||||||
|
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
self._logger.debug('Receiving updates timed out')
|
self._logger.debug('Receiving updates timed out')
|
||||||
|
|
||||||
except ReadCancelledError:
|
except ReadCancelledError:
|
||||||
self._logger.info('Receiving updates cancelled')
|
self._logger.debug('Receiving updates cancelled')
|
||||||
|
|
||||||
except BrokenPipeError:
|
except BrokenPipeError:
|
||||||
self._logger.info('Tcp session is broken. Reconnecting...')
|
self._logger.debug('Tcp session is broken. Reconnecting...')
|
||||||
self.reconnect()
|
self.reconnect()
|
||||||
|
|
||||||
except InvalidChecksumError:
|
except InvalidChecksumError:
|
||||||
self._logger.info('MTProto session is broken. Reconnecting...')
|
self._logger.debug('MTProto session is broken. Reconnecting...')
|
||||||
self.reconnect()
|
self.reconnect()
|
||||||
|
|
||||||
except OSError:
|
except OSError:
|
||||||
self._logger.warning('OSError on updates thread, %s logging out',
|
self._logger.debug('OSError on updates thread, %s logging out',
|
||||||
'was' if self.sender.logging_out else 'was not')
|
'was' if self._sender.logging_out else 'was not')
|
||||||
|
|
||||||
if self.sender.logging_out:
|
if self._sender.logging_out:
|
||||||
# This error is okay when logging out, means we got disconnected
|
# This error is okay when logging out, means we got disconnected
|
||||||
# TODO Not sure why this happens because we call disconnect()...
|
# TODO Not sure why this happens because we call disconnect()...
|
||||||
self._set_updates_thread(running=False)
|
self._set_updates_thread(running=False)
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
from .mtproto_request import MTProtoRequest
|
from .tlobject import TLObject
|
||||||
from .session import Session, JsonSession
|
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 os
|
||||||
import pickle
|
import pickle
|
||||||
import platform
|
import platform
|
||||||
import random
|
|
||||||
import time
|
import time
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from base64 import b64encode, b64decode
|
from base64 import b64encode, b64decode
|
||||||
|
@ -65,15 +64,10 @@ class Session:
|
||||||
return self.sequence * 2
|
return self.sequence * 2
|
||||||
|
|
||||||
def get_new_msg_id(self):
|
def get_new_msg_id(self):
|
||||||
"""Generates a new message ID based on the current time (in ms) since epoch"""
|
now = time.time()
|
||||||
# Refer to mtproto_plain_sender.py for the original method, this is a simple copy
|
nanoseconds = int((now - int(now)) * 1e+9)
|
||||||
ms_time = int(time.time() * 1000)
|
# "message identifiers are divisible by 4"
|
||||||
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32)
|
new_msg_id = (int(now) << 32) | (nanoseconds << 2)
|
||||||
| # "must approximately equal unix time*2^32"
|
|
||||||
((ms_time % 1000) << 22)
|
|
||||||
| # "approximate moment in time the message was created"
|
|
||||||
random.randint(0, 524288)
|
|
||||||
<< 2) # "message identifiers are divisible by 4"
|
|
||||||
|
|
||||||
if self.last_message_id >= new_msg_id:
|
if self.last_message_id >= new_msg_id:
|
||||||
new_msg_id = self.last_message_id + 4
|
new_msg_id = self.last_message_id + 4
|
||||||
|
@ -113,14 +107,19 @@ class JsonSession:
|
||||||
self.system_version = session.system_version
|
self.system_version = session.system_version
|
||||||
self.app_version = session.app_version
|
self.app_version = session.app_version
|
||||||
self.lang_code = session.lang_code
|
self.lang_code = session.lang_code
|
||||||
|
self.system_lang_code = session.system_lang_code
|
||||||
|
self.lang_pack = session.lang_pack
|
||||||
|
|
||||||
else: # str / None
|
else: # str / None
|
||||||
self.session_user_id = session_user_id
|
self.session_user_id = session_user_id
|
||||||
|
|
||||||
self.device_model = platform.node()
|
system = platform.uname()
|
||||||
self.system_version = platform.system()
|
self.device_model = system.system if system.system else 'Unknown'
|
||||||
self.app_version = '1.0' # note: '0' will provoke error
|
self.system_version = system.release if system.release else '1.0'
|
||||||
|
self.app_version = '1.0' # '0' will provoke error
|
||||||
self.lang_code = 'en'
|
self.lang_code = 'en'
|
||||||
|
self.system_lang_code = self.lang_code
|
||||||
|
self.lang_pack = ''
|
||||||
|
|
||||||
# Cross-thread safety
|
# Cross-thread safety
|
||||||
self._lock = Lock()
|
self._lock = Lock()
|
||||||
|
@ -133,7 +132,7 @@ class JsonSession:
|
||||||
self._sequence = 0
|
self._sequence = 0
|
||||||
self.salt = 0 # Unsigned long
|
self.salt = 0 # Unsigned long
|
||||||
self.time_offset = 0
|
self.time_offset = 0
|
||||||
self.last_message_id = 0 # Long
|
self._last_msg_id = 0 # Long
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Saves the current session object as session_user_id.session"""
|
"""Saves the current session object as session_user_id.session"""
|
||||||
|
@ -229,19 +228,18 @@ class JsonSession:
|
||||||
def get_new_msg_id(self):
|
def get_new_msg_id(self):
|
||||||
"""Generates a new unique message ID based on the current
|
"""Generates a new unique message ID based on the current
|
||||||
time (in ms) since epoch"""
|
time (in ms) since epoch"""
|
||||||
# Refer to mtproto_plain_sender.py for the original method,
|
# Refer to mtproto_plain_sender.py for the original method
|
||||||
ms_time = int(time.time() * 1000)
|
now = time.time()
|
||||||
new_msg_id = (((ms_time // 1000 + self.time_offset) << 32)
|
nanoseconds = int((now - int(now)) * 1e+9)
|
||||||
| # "must approximately equal unix time*2^32"
|
# "message identifiers are divisible by 4"
|
||||||
((ms_time % 1000) << 22)
|
new_msg_id = (int(now) << 32) | (nanoseconds << 2)
|
||||||
| # "approximate moment in time the message was created"
|
|
||||||
random.randint(0, 524288)
|
|
||||||
<< 2) # "message identifiers are divisible by 4"
|
|
||||||
|
|
||||||
if self.last_message_id >= new_msg_id:
|
with self._lock:
|
||||||
new_msg_id = self.last_message_id + 4
|
if self._last_msg_id >= new_msg_id:
|
||||||
|
new_msg_id = self._last_msg_id + 4
|
||||||
|
|
||||||
|
self._last_msg_id = new_msg_id
|
||||||
|
|
||||||
self.last_message_id = new_msg_id
|
|
||||||
return new_msg_id
|
return new_msg_id
|
||||||
|
|
||||||
def update_time_offset(self, correct_msg_id):
|
def update_time_offset(self, correct_msg_id):
|
||||||
|
|
113
telethon/tl/tlobject.py
Normal file
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 (
|
from .tl.types import (
|
||||||
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
||||||
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
|
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
|
||||||
InputPeerSelf, MessageMediaDocument, MessageMediaPhoto, PeerChannel,
|
MessageMediaDocument, MessageMediaPhoto, PeerChannel, InputChannel,
|
||||||
|
UserEmpty, InputUser, InputUserEmpty, InputUserSelf, InputPeerSelf,
|
||||||
PeerChat, PeerUser, User, UserFull, UserProfilePhoto)
|
PeerChat, PeerUser, User, UserFull, UserProfilePhoto)
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,11 +53,14 @@ def get_extension(media):
|
||||||
def get_input_peer(entity):
|
def get_input_peer(entity):
|
||||||
"""Gets the input peer for the given "entity" (user, chat or channel).
|
"""Gets the input peer for the given "entity" (user, chat or channel).
|
||||||
A ValueError is raised if the given entity isn't a supported type."""
|
A ValueError is raised if the given entity isn't a supported type."""
|
||||||
if type(entity).subclass_of_id == 0xc91c90b6: # crc32('InputUser')
|
if type(entity).subclass_of_id == 0xc91c90b6: # crc32(b'InputPeer')
|
||||||
return entity
|
return entity
|
||||||
|
|
||||||
if isinstance(entity, User):
|
if isinstance(entity, User):
|
||||||
return InputPeerUser(entity.id, entity.access_hash)
|
if entity.is_self:
|
||||||
|
return InputPeerSelf()
|
||||||
|
else:
|
||||||
|
return InputPeerUser(entity.id, entity.access_hash)
|
||||||
|
|
||||||
if any(isinstance(entity, c) for c in (
|
if any(isinstance(entity, c) for c in (
|
||||||
Chat, ChatEmpty, ChatForbidden)):
|
Chat, ChatEmpty, ChatForbidden)):
|
||||||
|
@ -67,16 +71,64 @@ def get_input_peer(entity):
|
||||||
return InputPeerChannel(entity.id, entity.access_hash)
|
return InputPeerChannel(entity.id, entity.access_hash)
|
||||||
|
|
||||||
# Less common cases
|
# Less common cases
|
||||||
|
if isinstance(entity, UserEmpty):
|
||||||
|
return InputPeerEmpty()
|
||||||
|
|
||||||
|
if isinstance(entity, InputUser):
|
||||||
|
return InputPeerUser(entity.user_id, entity.access_hash)
|
||||||
|
|
||||||
if isinstance(entity, UserFull):
|
if isinstance(entity, UserFull):
|
||||||
return InputPeerUser(entity.user.id, entity.user.access_hash)
|
return get_input_peer(entity.user)
|
||||||
|
|
||||||
if isinstance(entity, ChatFull):
|
if isinstance(entity, ChatFull):
|
||||||
return InputPeerChat(entity.id)
|
return InputPeerChat(entity.id)
|
||||||
|
|
||||||
|
if isinstance(entity, PeerChat):
|
||||||
|
return InputPeerChat(entity.chat_id)
|
||||||
|
|
||||||
raise ValueError('Cannot cast {} to any kind of InputPeer.'
|
raise ValueError('Cannot cast {} to any kind of InputPeer.'
|
||||||
.format(type(entity).__name__))
|
.format(type(entity).__name__))
|
||||||
|
|
||||||
|
|
||||||
|
def get_input_channel(entity):
|
||||||
|
"""Similar to get_input_peer, but for InputChannel's alone"""
|
||||||
|
if type(entity).subclass_of_id == 0x40f202fd: # crc32(b'InputChannel')
|
||||||
|
return entity
|
||||||
|
|
||||||
|
if isinstance(entity, Channel) or isinstance(entity, ChannelForbidden):
|
||||||
|
return InputChannel(entity.id, entity.access_hash)
|
||||||
|
|
||||||
|
if isinstance(entity, InputPeerChannel):
|
||||||
|
return InputChannel(entity.channel_id, entity.access_hash)
|
||||||
|
|
||||||
|
raise ValueError('Cannot cast {} to any kind of InputChannel.'
|
||||||
|
.format(type(entity).__name__))
|
||||||
|
|
||||||
|
|
||||||
|
def get_input_user(entity):
|
||||||
|
"""Similar to get_input_peer, but for InputUser's alone"""
|
||||||
|
if type(entity).subclass_of_id == 0xe669bf46: # crc32(b'InputUser')
|
||||||
|
return entity
|
||||||
|
|
||||||
|
if isinstance(entity, User):
|
||||||
|
if entity.is_self:
|
||||||
|
return InputUserSelf()
|
||||||
|
else:
|
||||||
|
return InputUser(entity.id, entity.access_hash)
|
||||||
|
|
||||||
|
if isinstance(entity, UserEmpty):
|
||||||
|
return InputUserEmpty()
|
||||||
|
|
||||||
|
if isinstance(entity, UserFull):
|
||||||
|
return get_input_user(entity.user)
|
||||||
|
|
||||||
|
if isinstance(entity, InputPeerUser):
|
||||||
|
return InputUser(entity.user_id, entity.access_hash)
|
||||||
|
|
||||||
|
raise ValueError('Cannot cast {} to any kind of InputUser.'
|
||||||
|
.format(type(entity).__name__))
|
||||||
|
|
||||||
|
|
||||||
def find_user_or_chat(peer, users, chats):
|
def find_user_or_chat(peer, users, chats):
|
||||||
"""Finds the corresponding user or chat given a peer.
|
"""Finds the corresponding user or chat given a peer.
|
||||||
Returns None if it was not found"""
|
Returns None if it was not found"""
|
||||||
|
|
|
@ -217,7 +217,7 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
# Send chat message (if any)
|
# Send chat message (if any)
|
||||||
elif msg:
|
elif msg:
|
||||||
self.send_message(
|
self.send_message(
|
||||||
entity, msg, no_web_page=True)
|
entity, msg, link_preview=False)
|
||||||
|
|
||||||
def send_photo(self, path, entity):
|
def send_photo(self, path, entity):
|
||||||
print('Uploading {}...'.format(path))
|
print('Uploading {}...'.format(path))
|
||||||
|
@ -250,7 +250,7 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
print('Downloading media with name {}...'.format(output))
|
print('Downloading media with name {}...'.format(output))
|
||||||
output = self.download_msg_media(
|
output = self.download_msg_media(
|
||||||
msg.media,
|
msg.media,
|
||||||
file_path=output,
|
file=output,
|
||||||
progress_callback=self.download_progress_callback)
|
progress_callback=self.download_progress_callback)
|
||||||
print('Media downloaded to {}!'.format(output))
|
print('Media downloaded to {}!'.format(output))
|
||||||
|
|
||||||
|
@ -275,7 +275,7 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_handler(update_object):
|
def update_handler(update_object):
|
||||||
if type(update_object) is UpdateShortMessage:
|
if isinstance(update_object, UpdateShortMessage):
|
||||||
if update_object.out:
|
if update_object.out:
|
||||||
sprint('You sent {} to user #{}'.format(
|
sprint('You sent {} to user #{}'.format(
|
||||||
update_object.message, update_object.user_id))
|
update_object.message, update_object.user_id))
|
||||||
|
@ -283,7 +283,7 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
sprint('[User #{} sent {}]'.format(
|
sprint('[User #{} sent {}]'.format(
|
||||||
update_object.user_id, update_object.message))
|
update_object.user_id, update_object.message))
|
||||||
|
|
||||||
elif type(update_object) is UpdateShortChatMessage:
|
elif isinstance(update_object, UpdateShortChatMessage):
|
||||||
if update_object.out:
|
if update_object.out:
|
||||||
sprint('You sent {} to chat #{}'.format(
|
sprint('You sent {} to chat #{}'.format(
|
||||||
update_object.message, update_object.chat_id))
|
update_object.message, update_object.chat_id))
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import re
|
import re
|
||||||
|
from zlib import crc32
|
||||||
|
|
||||||
|
|
||||||
class TLObject:
|
class TLObject:
|
||||||
|
@ -24,12 +25,18 @@ class TLObject:
|
||||||
self.namespace = None
|
self.namespace = None
|
||||||
self.name = fullname
|
self.name = fullname
|
||||||
|
|
||||||
# The ID should be an hexadecimal string
|
|
||||||
self.id = int(object_id, base=16)
|
|
||||||
self.args = args
|
self.args = args
|
||||||
self.result = result
|
self.result = result
|
||||||
self.is_function = is_function
|
self.is_function = is_function
|
||||||
|
|
||||||
|
# The ID should be an hexadecimal string or None to be inferred
|
||||||
|
if object_id is None:
|
||||||
|
self.id = self.infer_id()
|
||||||
|
else:
|
||||||
|
self.id = int(object_id, base=16)
|
||||||
|
assert self.id == self.infer_id(),\
|
||||||
|
'Invalid inferred ID for ' + repr(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_tl(tl, is_function):
|
def from_tl(tl, is_function):
|
||||||
"""Returns a TL object from the given TL scheme line"""
|
"""Returns a TL object from the given TL scheme line"""
|
||||||
|
@ -38,8 +45,10 @@ class TLObject:
|
||||||
match = re.match(r'''
|
match = re.match(r'''
|
||||||
^ # We want to match from the beginning to the end
|
^ # We want to match from the beginning to the end
|
||||||
([\w.]+) # The .tl object can contain alpha_name or namespace.alpha_name
|
([\w.]+) # The .tl object can contain alpha_name or namespace.alpha_name
|
||||||
\# # After the name, comes the ID of the object
|
(?:
|
||||||
([0-9a-f]+) # The constructor ID is in hexadecimal form
|
\# # After the name, comes the ID of the object
|
||||||
|
([0-9a-f]+) # The constructor ID is in hexadecimal form
|
||||||
|
)? # If no constructor ID was given, CRC32 the 'tl' to determine it
|
||||||
|
|
||||||
(?:\s # After that, we want to match its arguments (name:type)
|
(?:\s # After that, we want to match its arguments (name:type)
|
||||||
{? # For handling the start of the '{X:Type}' case
|
{? # For handling the start of the '{X:Type}' case
|
||||||
|
@ -91,16 +100,39 @@ class TLObject:
|
||||||
(and thus should be embedded in the generated code) or not"""
|
(and thus should be embedded in the generated code) or not"""
|
||||||
return self.id in TLObject.CORE_TYPES
|
return self.id in TLObject.CORE_TYPES
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self, ignore_id=False):
|
||||||
fullname = ('{}.{}'.format(self.namespace, self.name)
|
fullname = ('{}.{}'.format(self.namespace, self.name)
|
||||||
if self.namespace is not None else self.name)
|
if self.namespace is not None else self.name)
|
||||||
|
|
||||||
hex_id = hex(self.id)[2:].rjust(8,
|
if getattr(self, 'id', None) is None or ignore_id:
|
||||||
'0') # Skip 0x and add 0's for padding
|
hex_id = ''
|
||||||
|
else:
|
||||||
|
# Skip 0x and add 0's for padding
|
||||||
|
hex_id = '#' + hex(self.id)[2:].rjust(8, '0')
|
||||||
|
|
||||||
return '{}#{} {} = {}'.format(
|
if self.args:
|
||||||
fullname, hex_id, ' '.join([str(arg) for arg in self.args]),
|
args = ' ' + ' '.join([repr(arg) for arg in self.args])
|
||||||
self.result)
|
else:
|
||||||
|
args = ''
|
||||||
|
|
||||||
|
return '{}{}{} = {}'.format(fullname, hex_id, args, self.result)
|
||||||
|
|
||||||
|
def infer_id(self):
|
||||||
|
representation = self.__repr__(ignore_id=True)
|
||||||
|
|
||||||
|
# Clean the representation
|
||||||
|
representation = representation\
|
||||||
|
.replace(':bytes ', ':string ')\
|
||||||
|
.replace('?bytes ', '?string ')\
|
||||||
|
.replace('<', ' ').replace('>', '')\
|
||||||
|
.replace('{', '').replace('}', '')
|
||||||
|
|
||||||
|
representation = re.sub(
|
||||||
|
r' \w+:flags\.\d+\?true',
|
||||||
|
r'',
|
||||||
|
representation
|
||||||
|
)
|
||||||
|
return crc32(representation.encode('ascii'))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
fullname = ('{}.{}'.format(self.namespace, self.name)
|
fullname = ('{}.{}'.format(self.namespace, self.name)
|
||||||
|
@ -214,3 +246,9 @@ class TLArg:
|
||||||
return '{{{}:{}}}'.format(self.name, real_type)
|
return '{{{}:{}}}'.format(self.name, real_type)
|
||||||
else:
|
else:
|
||||||
return '{}:{}'.format(self.name, real_type)
|
return '{}:{}'.format(self.name, real_type)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# Get rid of our special type
|
||||||
|
return str(self)\
|
||||||
|
.replace(':date', ':int')\
|
||||||
|
.replace('?date', '?int')
|
||||||
|
|
|
@ -106,6 +106,9 @@ new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long =
|
||||||
|
|
||||||
http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait;
|
http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait;
|
||||||
|
|
||||||
|
ipPort ipv4:int port:int = IpPort;
|
||||||
|
help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector<ipPort> = help.ConfigSimple;
|
||||||
|
|
||||||
---functions---
|
---functions---
|
||||||
|
|
||||||
rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;
|
rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;
|
||||||
|
@ -152,17 +155,16 @@ inputFile#f52ff27f id:long parts:int name:string md5_checksum:string = InputFile
|
||||||
inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile;
|
inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile;
|
||||||
|
|
||||||
inputMediaEmpty#9664f57f = InputMedia;
|
inputMediaEmpty#9664f57f = InputMedia;
|
||||||
inputMediaUploadedPhoto#630c9af1 flags:# file:InputFile caption:string stickers:flags.0?Vector<InputDocument> = InputMedia;
|
inputMediaUploadedPhoto#2f37e231 flags:# file:InputFile caption:string stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||||
inputMediaPhoto#e9bfb4f3 id:InputPhoto caption:string = InputMedia;
|
inputMediaPhoto#81fa373a flags:# id:InputPhoto caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||||
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
|
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
|
||||||
inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia;
|
inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia;
|
||||||
inputMediaUploadedDocument#d070f1e9 flags:# file:InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> = InputMedia;
|
inputMediaUploadedDocument#e39621fd flags:# file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||||
inputMediaUploadedThumbDocument#50d88cae flags:# file:InputFile thumb:InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> = InputMedia;
|
inputMediaDocument#5acb668e flags:# id:InputDocument caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||||
inputMediaDocument#1a77f29c id:InputDocument caption:string = InputMedia;
|
|
||||||
inputMediaVenue#2827a81a geo_point:InputGeoPoint title:string address:string provider:string venue_id:string = InputMedia;
|
inputMediaVenue#2827a81a geo_point:InputGeoPoint title:string address:string provider:string venue_id:string = InputMedia;
|
||||||
inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
|
inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
|
||||||
inputMediaPhotoExternal#b55f4f18 url:string caption:string = InputMedia;
|
inputMediaPhotoExternal#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||||
inputMediaDocumentExternal#e5e9607c url:string caption:string = InputMedia;
|
inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||||
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
|
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
|
||||||
inputMediaInvoice#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia;
|
inputMediaInvoice#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia;
|
||||||
|
|
||||||
|
@ -216,11 +218,11 @@ userStatusLastMonth#77ebc742 = UserStatus;
|
||||||
chatEmpty#9ba2d800 id:int = Chat;
|
chatEmpty#9ba2d800 id:int = Chat;
|
||||||
chat#d91cdd54 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true admins_enabled:flags.3?true admin:flags.4?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel = Chat;
|
chat#d91cdd54 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true admins_enabled:flags.3?true admin:flags.4?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel = Chat;
|
||||||
chatForbidden#7328bdb id:int title:string = Chat;
|
chatForbidden#7328bdb id:int title:string = Chat;
|
||||||
channel#a14dca52 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true editor:flags.3?true moderator:flags.4?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string = Chat;
|
channel#cb44b1c flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights = Chat;
|
||||||
channelForbidden#8537784f flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string = Chat;
|
channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat;
|
||||||
|
|
||||||
chatFull#2e02a614 id:int participants:ChatParticipants chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> = ChatFull;
|
chatFull#2e02a614 id:int participants:ChatParticipants chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> = ChatFull;
|
||||||
channelFull#c3d5512f flags:# can_view_participants:flags.3?true can_set_username:flags.6?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int = ChatFull;
|
channelFull#95cb5f57 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int = ChatFull;
|
||||||
|
|
||||||
chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant;
|
chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant;
|
||||||
chatParticipantCreator#da13538a user_id:int = ChatParticipant;
|
chatParticipantCreator#da13538a user_id:int = ChatParticipant;
|
||||||
|
@ -233,15 +235,15 @@ chatPhotoEmpty#37c1011c = ChatPhoto;
|
||||||
chatPhoto#6153276a photo_small:FileLocation photo_big:FileLocation = ChatPhoto;
|
chatPhoto#6153276a photo_small:FileLocation photo_big:FileLocation = ChatPhoto;
|
||||||
|
|
||||||
messageEmpty#83e5de54 id:int = Message;
|
messageEmpty#83e5de54 id:int = Message;
|
||||||
message#c09be45f flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int = Message;
|
message#90dddc11 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string = Message;
|
||||||
messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message;
|
messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message;
|
||||||
|
|
||||||
messageMediaEmpty#3ded6320 = MessageMedia;
|
messageMediaEmpty#3ded6320 = MessageMedia;
|
||||||
messageMediaPhoto#3d8ce53d photo:Photo caption:string = MessageMedia;
|
messageMediaPhoto#b5223b0f flags:# photo:flags.0?Photo caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia;
|
||||||
messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia;
|
messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia;
|
||||||
messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:string user_id:int = MessageMedia;
|
messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:string user_id:int = MessageMedia;
|
||||||
messageMediaUnsupported#9f84f49e = MessageMedia;
|
messageMediaUnsupported#9f84f49e = MessageMedia;
|
||||||
messageMediaDocument#f3e02ea8 document:Document caption:string = MessageMedia;
|
messageMediaDocument#7c4414d3 flags:# document:flags.0?Document caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia;
|
||||||
messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia;
|
messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia;
|
||||||
messageMediaVenue#7912b71f geo:GeoPoint title:string address:string provider:string venue_id:string = MessageMedia;
|
messageMediaVenue#7912b71f geo:GeoPoint title:string address:string provider:string venue_id:string = MessageMedia;
|
||||||
messageMediaGame#fdb19008 game:Game = MessageMedia;
|
messageMediaGame#fdb19008 game:Game = MessageMedia;
|
||||||
|
@ -264,6 +266,7 @@ messageActionGameScore#92a72876 game_id:long score:int = MessageAction;
|
||||||
messageActionPaymentSentMe#8f31b327 flags:# currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge = MessageAction;
|
messageActionPaymentSentMe#8f31b327 flags:# currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge = MessageAction;
|
||||||
messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAction;
|
messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAction;
|
||||||
messageActionPhoneCall#80e11a7f flags:# call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction;
|
messageActionPhoneCall#80e11a7f flags:# call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction;
|
||||||
|
messageActionScreenshotTaken#4792929b = MessageAction;
|
||||||
|
|
||||||
dialog#66ffba14 flags:# pinned:flags.2?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage = Dialog;
|
dialog#66ffba14 flags:# pinned:flags.2?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage = Dialog;
|
||||||
|
|
||||||
|
@ -326,7 +329,7 @@ contacts.link#3ace484c my_link:ContactLink foreign_link:ContactLink user:User =
|
||||||
contacts.contactsNotModified#b74ba9d2 = contacts.Contacts;
|
contacts.contactsNotModified#b74ba9d2 = contacts.Contacts;
|
||||||
contacts.contacts#6f8b8cb2 contacts:Vector<Contact> users:Vector<User> = contacts.Contacts;
|
contacts.contacts#6f8b8cb2 contacts:Vector<Contact> users:Vector<User> = contacts.Contacts;
|
||||||
|
|
||||||
contacts.importedContacts#ad524315 imported:Vector<ImportedContact> retry_contacts:Vector<long> users:Vector<User> = contacts.ImportedContacts;
|
contacts.importedContacts#77d01c3b imported:Vector<ImportedContact> popular_invites:Vector<PopularContact> retry_contacts:Vector<long> users:Vector<User> = contacts.ImportedContacts;
|
||||||
|
|
||||||
contacts.blocked#1c138d15 blocked:Vector<ContactBlocked> users:Vector<User> = contacts.Blocked;
|
contacts.blocked#1c138d15 blocked:Vector<ContactBlocked> users:Vector<User> = contacts.Blocked;
|
||||||
contacts.blockedSlice#900802a1 count:int blocked:Vector<ContactBlocked> users:Vector<User> = contacts.Blocked;
|
contacts.blockedSlice#900802a1 count:int blocked:Vector<ContactBlocked> users:Vector<User> = contacts.Blocked;
|
||||||
|
@ -420,6 +423,8 @@ updateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Upd
|
||||||
updateBotShippingQuery#e0cdc940 query_id:long user_id:int payload:bytes shipping_address:PostAddress = Update;
|
updateBotShippingQuery#e0cdc940 query_id:long user_id:int payload:bytes shipping_address:PostAddress = Update;
|
||||||
updateBotPrecheckoutQuery#5d2f3aa9 flags:# query_id:long user_id:int payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update;
|
updateBotPrecheckoutQuery#5d2f3aa9 flags:# query_id:long user_id:int payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update;
|
||||||
updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update;
|
updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update;
|
||||||
|
updateLangPackTooLong#10c2404b = Update;
|
||||||
|
updateLangPack#56022f4d difference:LangPackDifference = Update;
|
||||||
|
|
||||||
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
|
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
|
||||||
|
|
||||||
|
@ -442,11 +447,11 @@ photos.photosSlice#15051f54 count:int photos:Vector<Photo> users:Vector<User> =
|
||||||
photos.photo#20212ca8 photo:Photo users:Vector<User> = photos.Photo;
|
photos.photo#20212ca8 photo:Photo users:Vector<User> = photos.Photo;
|
||||||
|
|
||||||
upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File;
|
upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File;
|
||||||
upload.fileCdnRedirect#1508485a dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes = upload.File;
|
upload.fileCdnRedirect#ea52fe5a dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes cdn_file_hashes:Vector<CdnFileHash> = upload.File;
|
||||||
|
|
||||||
dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true id:int ip_address:string port:int = DcOption;
|
dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int = DcOption;
|
||||||
|
|
||||||
config#cb601684 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string disabled_features:Vector<DisabledFeature> = Config;
|
config#7feec888 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector<DisabledFeature> = Config;
|
||||||
|
|
||||||
nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;
|
nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;
|
||||||
|
|
||||||
|
@ -644,19 +649,16 @@ channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:
|
||||||
|
|
||||||
channelParticipant#15ebac1d user_id:int date:int = ChannelParticipant;
|
channelParticipant#15ebac1d user_id:int date:int = ChannelParticipant;
|
||||||
channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelParticipant;
|
channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelParticipant;
|
||||||
channelParticipantModerator#91057fef user_id:int inviter_id:int date:int = ChannelParticipant;
|
|
||||||
channelParticipantEditor#98192d61 user_id:int inviter_id:int date:int = ChannelParticipant;
|
|
||||||
channelParticipantKicked#8cc5e69a user_id:int kicked_by:int date:int = ChannelParticipant;
|
|
||||||
channelParticipantCreator#e3e2e1f9 user_id:int = ChannelParticipant;
|
channelParticipantCreator#e3e2e1f9 user_id:int = ChannelParticipant;
|
||||||
|
channelParticipantAdmin#a82fa898 flags:# can_edit:flags.0?true user_id:int inviter_id:int promoted_by:int date:int admin_rights:ChannelAdminRights = ChannelParticipant;
|
||||||
|
channelParticipantBanned#222c1886 flags:# left:flags.0?true user_id:int kicked_by:int date:int banned_rights:ChannelBannedRights = ChannelParticipant;
|
||||||
|
|
||||||
channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter;
|
channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter;
|
||||||
channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter;
|
channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter;
|
||||||
channelParticipantsKicked#3c37bb7a = ChannelParticipantsFilter;
|
channelParticipantsKicked#a3b54985 q:string = ChannelParticipantsFilter;
|
||||||
channelParticipantsBots#b0d1865b = ChannelParticipantsFilter;
|
channelParticipantsBots#b0d1865b = ChannelParticipantsFilter;
|
||||||
|
channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter;
|
||||||
channelRoleEmpty#b285a0c6 = ChannelParticipantRole;
|
channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter;
|
||||||
channelRoleModerator#9618d975 = ChannelParticipantRole;
|
|
||||||
channelRoleEditor#820bfe8c = ChannelParticipantRole;
|
|
||||||
|
|
||||||
channels.channelParticipants#f56ee2a8 count:int participants:Vector<ChannelParticipant> users:Vector<User> = channels.ChannelParticipants;
|
channels.channelParticipants#f56ee2a8 count:int participants:Vector<ChannelParticipant> users:Vector<User> = channels.ChannelParticipants;
|
||||||
|
|
||||||
|
@ -697,7 +699,7 @@ messages.botResults#ccd3563d flags:# gallery:flags.0?true query_id:long next_off
|
||||||
|
|
||||||
exportedMessageLink#1f486803 link:string = ExportedMessageLink;
|
exportedMessageLink#1f486803 link:string = ExportedMessageLink;
|
||||||
|
|
||||||
messageFwdHeader#c786ddcb flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int = MessageFwdHeader;
|
messageFwdHeader#fadff4ac flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string = MessageFwdHeader;
|
||||||
|
|
||||||
auth.codeTypeSms#72a3158c = auth.CodeType;
|
auth.codeTypeSms#72a3158c = auth.CodeType;
|
||||||
auth.codeTypeCall#741cd3e3 = auth.CodeType;
|
auth.codeTypeCall#741cd3e3 = auth.CodeType;
|
||||||
|
@ -725,6 +727,7 @@ topPeerCategoryBotsInline#148677e2 = TopPeerCategory;
|
||||||
topPeerCategoryCorrespondents#637b7ed = TopPeerCategory;
|
topPeerCategoryCorrespondents#637b7ed = TopPeerCategory;
|
||||||
topPeerCategoryGroups#bd17a14a = TopPeerCategory;
|
topPeerCategoryGroups#bd17a14a = TopPeerCategory;
|
||||||
topPeerCategoryChannels#161d9628 = TopPeerCategory;
|
topPeerCategoryChannels#161d9628 = TopPeerCategory;
|
||||||
|
topPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory;
|
||||||
|
|
||||||
topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector<TopPeer> = TopPeerCategoryPeers;
|
topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector<TopPeer> = TopPeerCategoryPeers;
|
||||||
|
|
||||||
|
@ -795,9 +798,10 @@ pageBlockEmbedPost#292c7be9 url:string webpage_id:long author_photo_id:long auth
|
||||||
pageBlockCollage#8b31c4f items:Vector<PageBlock> caption:RichText = PageBlock;
|
pageBlockCollage#8b31c4f items:Vector<PageBlock> caption:RichText = PageBlock;
|
||||||
pageBlockSlideshow#130c8963 items:Vector<PageBlock> caption:RichText = PageBlock;
|
pageBlockSlideshow#130c8963 items:Vector<PageBlock> caption:RichText = PageBlock;
|
||||||
pageBlockChannel#ef1751b5 channel:Chat = PageBlock;
|
pageBlockChannel#ef1751b5 channel:Chat = PageBlock;
|
||||||
|
pageBlockAudio#31b81a7f audio_id:long caption:RichText = PageBlock;
|
||||||
|
|
||||||
pagePart#8dee6c44 blocks:Vector<PageBlock> photos:Vector<Photo> videos:Vector<Document> = Page;
|
pagePart#8e3f9ebe blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> = Page;
|
||||||
pageFull#d7a19d69 blocks:Vector<PageBlock> photos:Vector<Photo> videos:Vector<Document> = Page;
|
pageFull#556ec7aa blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> = Page;
|
||||||
|
|
||||||
phoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason;
|
phoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason;
|
||||||
phoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason;
|
phoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason;
|
||||||
|
@ -844,6 +848,8 @@ account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPas
|
||||||
|
|
||||||
shippingOption#b6213cdf id:string title:string prices:Vector<LabeledPrice> = ShippingOption;
|
shippingOption#b6213cdf id:string title:string prices:Vector<LabeledPrice> = ShippingOption;
|
||||||
|
|
||||||
|
inputStickerSetItem#ffa0a496 flags:# document:InputDocument emoji:string mask_coords:flags.0?MaskCoords = InputStickerSetItem;
|
||||||
|
|
||||||
inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall;
|
inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall;
|
||||||
|
|
||||||
phoneCallEmpty#5366c915 id:long = PhoneCall;
|
phoneCallEmpty#5366c915 id:long = PhoneCall;
|
||||||
|
@ -866,11 +872,48 @@ cdnPublicKey#c982eaba dc_id:int public_key:string = CdnPublicKey;
|
||||||
|
|
||||||
cdnConfig#5725e40a public_keys:Vector<CdnPublicKey> = CdnConfig;
|
cdnConfig#5725e40a public_keys:Vector<CdnPublicKey> = CdnConfig;
|
||||||
|
|
||||||
|
langPackString#cad181f6 key:string value:string = LangPackString;
|
||||||
|
langPackStringPluralized#6c47ac9f flags:# key:string zero_value:flags.0?string one_value:flags.1?string two_value:flags.2?string few_value:flags.3?string many_value:flags.4?string other_value:string = LangPackString;
|
||||||
|
langPackStringDeleted#2979eeb2 key:string = LangPackString;
|
||||||
|
|
||||||
|
langPackDifference#f385c1f6 lang_code:string from_version:int version:int strings:Vector<LangPackString> = LangPackDifference;
|
||||||
|
|
||||||
|
langPackLanguage#117698f1 name:string native_name:string lang_code:string = LangPackLanguage;
|
||||||
|
|
||||||
|
channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true = ChannelAdminRights;
|
||||||
|
|
||||||
|
channelBannedRights#58cf4249 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true until_date:int = ChannelBannedRights;
|
||||||
|
|
||||||
|
channelAdminLogEventActionChangeTitle#e6dfb825 prev_value:string new_value:string = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionChangeAbout#55188a2e prev_value:string new_value:string = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionChangeUsername#6a4afc38 prev_value:string new_value:string = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionChangePhoto#b82f55c3 prev_photo:ChatPhoto new_photo:ChatPhoto = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionToggleInvites#1b7907ae new_value:Bool = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionToggleSignatures#26ae0971 new_value:Bool = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionUpdatePinned#e9e82c18 message:Message = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionEditMessage#709b2405 prev_message:Message new_message:Message = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionDeleteMessage#42e047bb message:Message = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionParticipantJoin#183040d3 = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionParticipantLeave#f89777f2 = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticipant = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionParticipantToggleBan#e6d83d7e prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;
|
||||||
|
channelAdminLogEventActionParticipantToggleAdmin#d5676710 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;
|
||||||
|
|
||||||
|
channelAdminLogEvent#3b5a3e40 id:long date:int user_id:int action:ChannelAdminLogEventAction = ChannelAdminLogEvent;
|
||||||
|
|
||||||
|
channels.adminLogResults#ed8af74d events:Vector<ChannelAdminLogEvent> chats:Vector<Chat> users:Vector<User> = channels.AdminLogResults;
|
||||||
|
|
||||||
|
channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true = ChannelAdminLogEventsFilter;
|
||||||
|
|
||||||
|
popularContact#5ce14175 client_id:long importers:int = PopularContact;
|
||||||
|
|
||||||
|
cdnFileHash#77eec38f offset:int limit:int hash:bytes = CdnFileHash;
|
||||||
|
|
||||||
---functions---
|
---functions---
|
||||||
|
|
||||||
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
|
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
|
||||||
invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector<long> query:!X = X;
|
invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector<long> query:!X = X;
|
||||||
initConnection#69796de9 {X:Type} api_id:int device_model:string system_version:string app_version:string lang_code:string query:!X = X;
|
initConnection#c7481da6 {X:Type} api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string query:!X = X;
|
||||||
invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;
|
invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;
|
||||||
invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;
|
invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;
|
||||||
|
|
||||||
|
@ -935,13 +978,13 @@ contacts.exportCard#84e53737 = Vector<int>;
|
||||||
contacts.importCard#4fe196fe export_card:Vector<int> = User;
|
contacts.importCard#4fe196fe export_card:Vector<int> = User;
|
||||||
contacts.search#11f812d8 q:string limit:int = contacts.Found;
|
contacts.search#11f812d8 q:string limit:int = contacts.Found;
|
||||||
contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer;
|
contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer;
|
||||||
contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:int = contacts.TopPeers;
|
contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:int = contacts.TopPeers;
|
||||||
contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;
|
contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;
|
||||||
|
|
||||||
messages.getMessages#4222fa74 id:Vector<int> = messages.Messages;
|
messages.getMessages#4222fa74 id:Vector<int> = messages.Messages;
|
||||||
messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs;
|
messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs;
|
||||||
messages.getHistory#afa92846 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
messages.getHistory#afa92846 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
||||||
messages.search#d4569248 flags:# peer:InputPeer q:string filter:MessagesFilter min_date:int max_date:int offset:int max_id:int limit:int = messages.Messages;
|
messages.search#f288a275 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset:int max_id:int limit:int = messages.Messages;
|
||||||
messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages;
|
messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages;
|
||||||
messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true peer:InputPeer max_id:int = messages.AffectedHistory;
|
messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true peer:InputPeer max_id:int = messages.AffectedHistory;
|
||||||
messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages;
|
messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages;
|
||||||
|
@ -1023,6 +1066,8 @@ messages.reorderPinnedDialogs#959ff644 flags:# force:flags.0?true order:Vector<I
|
||||||
messages.getPinnedDialogs#e254d64e = messages.PeerDialogs;
|
messages.getPinnedDialogs#e254d64e = messages.PeerDialogs;
|
||||||
messages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector<ShippingOption> = Bool;
|
messages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector<ShippingOption> = Bool;
|
||||||
messages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool;
|
messages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool;
|
||||||
|
messages.uploadMedia#519bc2b1 peer:InputPeer media:InputMedia = MessageMedia;
|
||||||
|
messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int random_id:long = Updates;
|
||||||
|
|
||||||
updates.getState#edd4882a = updates.State;
|
updates.getState#edd4882a = updates.State;
|
||||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||||
|
@ -1038,7 +1083,8 @@ upload.getFile#e3a6cfb5 location:InputFileLocation offset:int limit:int = upload
|
||||||
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
|
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
|
||||||
upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
|
upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
|
||||||
upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile;
|
upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile;
|
||||||
upload.reuploadCdnFile#2e7a2020 file_token:bytes request_token:bytes = Bool;
|
upload.reuploadCdnFile#1af91c09 file_token:bytes request_token:bytes = Vector<CdnFileHash>;
|
||||||
|
upload.getCdnFileHashes#f715c87b file_token:bytes offset:int = Vector<CdnFileHash>;
|
||||||
|
|
||||||
help.getConfig#c4f9186b = Config;
|
help.getConfig#c4f9186b = Config;
|
||||||
help.getNearestDc#1fb33026 = NearestDc;
|
help.getNearestDc#1fb33026 = NearestDc;
|
||||||
|
@ -1062,7 +1108,7 @@ channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;
|
||||||
channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull;
|
channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull;
|
||||||
channels.createChannel#f4893d7f flags:# broadcast:flags.0?true megagroup:flags.1?true title:string about:string = Updates;
|
channels.createChannel#f4893d7f flags:# broadcast:flags.0?true megagroup:flags.1?true title:string about:string = Updates;
|
||||||
channels.editAbout#13e27f1e channel:InputChannel about:string = Bool;
|
channels.editAbout#13e27f1e channel:InputChannel about:string = Bool;
|
||||||
channels.editAdmin#eb7611d0 channel:InputChannel user_id:InputUser role:ChannelParticipantRole = Updates;
|
channels.editAdmin#20b88214 channel:InputChannel user_id:InputUser admin_rights:ChannelAdminRights = Updates;
|
||||||
channels.editTitle#566decd0 channel:InputChannel title:string = Updates;
|
channels.editTitle#566decd0 channel:InputChannel title:string = Updates;
|
||||||
channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates;
|
channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates;
|
||||||
channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool;
|
channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool;
|
||||||
|
@ -1070,7 +1116,6 @@ channels.updateUsername#3514b3de channel:InputChannel username:string = Bool;
|
||||||
channels.joinChannel#24b524c5 channel:InputChannel = Updates;
|
channels.joinChannel#24b524c5 channel:InputChannel = Updates;
|
||||||
channels.leaveChannel#f836aa95 channel:InputChannel = Updates;
|
channels.leaveChannel#f836aa95 channel:InputChannel = Updates;
|
||||||
channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector<InputUser> = Updates;
|
channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector<InputUser> = Updates;
|
||||||
channels.kickFromChannel#a672de14 channel:InputChannel user_id:InputUser kicked:Bool = Updates;
|
|
||||||
channels.exportInvite#c7560885 channel:InputChannel = ExportedChatInvite;
|
channels.exportInvite#c7560885 channel:InputChannel = ExportedChatInvite;
|
||||||
channels.deleteChannel#c0111fe3 channel:InputChannel = Updates;
|
channels.deleteChannel#c0111fe3 channel:InputChannel = Updates;
|
||||||
channels.toggleInvites#49609307 channel:InputChannel enabled:Bool = Updates;
|
channels.toggleInvites#49609307 channel:InputChannel enabled:Bool = Updates;
|
||||||
|
@ -1078,6 +1123,8 @@ channels.exportMessageLink#c846d22d channel:InputChannel id:int = ExportedMessag
|
||||||
channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates;
|
channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates;
|
||||||
channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates;
|
channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates;
|
||||||
channels.getAdminedPublicChannels#8d8d82d7 = messages.Chats;
|
channels.getAdminedPublicChannels#8d8d82d7 = messages.Chats;
|
||||||
|
channels.editBanned#bfd915cd channel:InputChannel user_id:InputUser banned_rights:ChannelBannedRights = Updates;
|
||||||
|
channels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector<InputUser> max_id:long min_id:long limit:int = channels.AdminLogResults;
|
||||||
|
|
||||||
bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON;
|
bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON;
|
||||||
bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool;
|
bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool;
|
||||||
|
@ -1089,6 +1136,11 @@ payments.sendPaymentForm#2b8879b3 flags:# msg_id:int requested_info_id:flags.0?s
|
||||||
payments.getSavedInfo#227d824b = payments.SavedInfo;
|
payments.getSavedInfo#227d824b = payments.SavedInfo;
|
||||||
payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool;
|
payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool;
|
||||||
|
|
||||||
|
stickers.createStickerSet#9bd86e6a flags:# masks:flags.0?true user_id:InputUser title:string short_name:string stickers:Vector<InputStickerSetItem> = messages.StickerSet;
|
||||||
|
stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet;
|
||||||
|
stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet;
|
||||||
|
stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet;
|
||||||
|
|
||||||
phone.getCallConfig#55451fa9 = DataJSON;
|
phone.getCallConfig#55451fa9 = DataJSON;
|
||||||
phone.requestCall#5b95b3d4 user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
phone.requestCall#5b95b3d4 user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||||
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||||
|
@ -1098,4 +1150,9 @@ phone.discardCall#78d413a6 peer:InputPhoneCall duration:int reason:PhoneCallDisc
|
||||||
phone.setCallRating#1c536a34 peer:InputPhoneCall rating:int comment:string = Updates;
|
phone.setCallRating#1c536a34 peer:InputPhoneCall rating:int comment:string = Updates;
|
||||||
phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool;
|
phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool;
|
||||||
|
|
||||||
// LAYER 66
|
langpack.getLangPack#9ab5c58e lang_code:string = LangPackDifference;
|
||||||
|
langpack.getStrings#2e1ee318 lang_code:string keys:Vector<string> = Vector<LangPackString>;
|
||||||
|
langpack.getDifference#b2e4d7d from_version:int = LangPackDifference;
|
||||||
|
langpack.getLanguages#800fd57d = Vector<LangPackLanguage>;
|
||||||
|
|
||||||
|
// LAYER 70
|
||||||
|
|
163
telethon_generator/tl_generator.py
Executable file → Normal file
163
telethon_generator/tl_generator.py
Executable file → Normal file
|
@ -1,51 +1,46 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from zlib import crc32
|
from zlib import crc32
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
try:
|
from .parser import SourceBuilder, TLParser
|
||||||
from .parser import SourceBuilder, TLParser
|
|
||||||
except (ImportError, SystemError):
|
|
||||||
from parser import SourceBuilder, TLParser
|
|
||||||
|
|
||||||
|
|
||||||
def get_output_path(normal_path):
|
|
||||||
return os.path.join('../telethon/tl', normal_path)
|
|
||||||
|
|
||||||
output_base_depth = 2 # telethon/tl/
|
|
||||||
|
|
||||||
|
|
||||||
class TLGenerator:
|
class TLGenerator:
|
||||||
@staticmethod
|
def __init__(self, output_dir):
|
||||||
def tlobjects_exist():
|
self.output_dir = output_dir
|
||||||
|
|
||||||
|
def _get_file(self, *paths):
|
||||||
|
return os.path.join(self.output_dir, *paths)
|
||||||
|
|
||||||
|
def _rm_if_exists(self, filename):
|
||||||
|
file = self._get_file(filename)
|
||||||
|
if os.path.exists(file):
|
||||||
|
if os.path.isdir(file):
|
||||||
|
shutil.rmtree(file)
|
||||||
|
else:
|
||||||
|
os.remove(file)
|
||||||
|
|
||||||
|
def tlobjects_exist(self):
|
||||||
"""Determines whether the TLObjects were previously
|
"""Determines whether the TLObjects were previously
|
||||||
generated (hence exist) or not
|
generated (hence exist) or not
|
||||||
"""
|
"""
|
||||||
return os.path.isfile(get_output_path('all_tlobjects.py'))
|
return os.path.isfile(self._get_file('all_tlobjects.py'))
|
||||||
|
|
||||||
@staticmethod
|
def clean_tlobjects(self):
|
||||||
def clean_tlobjects():
|
|
||||||
"""Cleans the automatically generated TLObjects from disk"""
|
"""Cleans the automatically generated TLObjects from disk"""
|
||||||
if os.path.isdir(get_output_path('functions')):
|
for name in ('functions', 'types', 'all_tlobjects.py'):
|
||||||
shutil.rmtree(get_output_path('functions'))
|
self._rm_if_exists(name)
|
||||||
|
|
||||||
if os.path.isdir(get_output_path('types')):
|
def generate_tlobjects(self, scheme_file, import_depth):
|
||||||
shutil.rmtree(get_output_path('types'))
|
|
||||||
|
|
||||||
if os.path.isfile(get_output_path('all_tlobjects.py')):
|
|
||||||
os.remove(get_output_path('all_tlobjects.py'))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def generate_tlobjects(scheme_file):
|
|
||||||
"""Generates all the TLObjects from scheme.tl to
|
"""Generates all the TLObjects from scheme.tl to
|
||||||
tl/functions and tl/types
|
tl/functions and tl/types
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# First ensure that the required parent directories exist
|
# First ensure that the required parent directories exist
|
||||||
os.makedirs(get_output_path('functions'), exist_ok=True)
|
os.makedirs(self._get_file('functions'), exist_ok=True)
|
||||||
os.makedirs(get_output_path('types'), exist_ok=True)
|
os.makedirs(self._get_file('types'), exist_ok=True)
|
||||||
|
|
||||||
# Step 0: Cache the parsed file on a tuple
|
# Step 0: Cache the parsed file on a tuple
|
||||||
tlobjects = tuple(TLParser.parse_file(scheme_file))
|
tlobjects = tuple(TLParser.parse_file(scheme_file))
|
||||||
|
@ -91,11 +86,11 @@ class TLGenerator:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Determine the output directory and create it
|
# Determine the output directory and create it
|
||||||
out_dir = get_output_path('functions'
|
out_dir = self._get_file('functions'
|
||||||
if tlobject.is_function else 'types')
|
if tlobject.is_function else 'types')
|
||||||
|
|
||||||
# Path depth to perform relative import
|
# Path depth to perform relative import
|
||||||
depth = output_base_depth
|
depth = import_depth
|
||||||
if tlobject.namespace:
|
if tlobject.namespace:
|
||||||
depth += 1
|
depth += 1
|
||||||
out_dir = os.path.join(out_dir, tlobject.namespace)
|
out_dir = os.path.join(out_dir, tlobject.namespace)
|
||||||
|
@ -121,19 +116,19 @@ class TLGenerator:
|
||||||
tlobject, builder, depth, type_constructors)
|
tlobject, builder, depth, type_constructors)
|
||||||
|
|
||||||
# Step 3: Add the relative imports to the namespaces on __init__.py's
|
# Step 3: Add the relative imports to the namespaces on __init__.py's
|
||||||
init_py = os.path.join(get_output_path('functions'), '__init__.py')
|
init_py = self._get_file('functions', '__init__.py')
|
||||||
with open(init_py, 'a') as file:
|
with open(init_py, 'a') as file:
|
||||||
file.write('from . import {}\n'
|
file.write('from . import {}\n'
|
||||||
.format(', '.join(function_namespaces)))
|
.format(', '.join(function_namespaces)))
|
||||||
|
|
||||||
init_py = os.path.join(get_output_path('types'), '__init__.py')
|
init_py = self._get_file('types', '__init__.py')
|
||||||
with open(init_py, 'a') as file:
|
with open(init_py, 'a') as file:
|
||||||
file.write('from . import {}\n'
|
file.write('from . import {}\n'
|
||||||
.format(', '.join(type_namespaces)))
|
.format(', '.join(type_namespaces)))
|
||||||
|
|
||||||
# Step 4: Once all the objects have been generated,
|
# Step 4: Once all the objects have been generated,
|
||||||
# we can now group them in a single file
|
# we can now group them in a single file
|
||||||
filename = os.path.join(get_output_path('all_tlobjects.py'))
|
filename = os.path.join(self._get_file('all_tlobjects.py'))
|
||||||
with open(filename, 'w', encoding='utf-8') as file:
|
with open(filename, 'w', encoding='utf-8') as file:
|
||||||
with SourceBuilder(file) as builder:
|
with SourceBuilder(file) as builder:
|
||||||
builder.writeln(
|
builder.writeln(
|
||||||
|
@ -182,17 +177,27 @@ class TLGenerator:
|
||||||
importing and documentation strings.
|
importing and documentation strings.
|
||||||
'"""
|
'"""
|
||||||
|
|
||||||
# Both types and functions inherit from
|
# Both types and functions inherit from the TLObject class so they
|
||||||
# MTProtoRequest so they all can be sent
|
# all can be serialized and sent, however, only the functions are
|
||||||
builder.writeln('from {}.tl.mtproto_request import MTProtoRequest'
|
# "content_related".
|
||||||
|
builder.writeln('from {}.tl.tlobject import TLObject'
|
||||||
.format('.' * depth))
|
.format('.' * depth))
|
||||||
|
|
||||||
if tlobject.is_function and \
|
if tlobject.is_function:
|
||||||
any(a for a in tlobject.args if a.type == 'InputPeer'):
|
util_imports = set()
|
||||||
# We can automatically convert a normal peer to an InputPeer,
|
for a in tlobject.args:
|
||||||
# it will make invoking a lot of requests a lot simpler.
|
# We can automatically convert some "full" types to
|
||||||
builder.writeln('from {}.utils import get_input_peer'
|
# "input only" (like User -> InputPeerUser, etc.)
|
||||||
.format('.' * depth))
|
if a.type == 'InputPeer':
|
||||||
|
util_imports.add('get_input_peer')
|
||||||
|
elif a.type == 'InputChannel':
|
||||||
|
util_imports.add('get_input_channel')
|
||||||
|
elif a.type == 'InputUser':
|
||||||
|
util_imports.add('get_input_user')
|
||||||
|
|
||||||
|
if util_imports:
|
||||||
|
builder.writeln('from {}.utils import {}'.format(
|
||||||
|
'.' * depth, ', '.join(util_imports)))
|
||||||
|
|
||||||
if any(a for a in tlobject.args if a.can_be_inferred):
|
if any(a for a in tlobject.args if a.can_be_inferred):
|
||||||
# Currently only 'random_id' needs 'os' to be imported
|
# Currently only 'random_id' needs 'os' to be imported
|
||||||
|
@ -200,7 +205,7 @@ class TLGenerator:
|
||||||
|
|
||||||
builder.writeln()
|
builder.writeln()
|
||||||
builder.writeln()
|
builder.writeln()
|
||||||
builder.writeln('class {}(MTProtoRequest):'.format(
|
builder.writeln('class {}(TLObject):'.format(
|
||||||
TLGenerator.get_class_name(tlobject)))
|
TLGenerator.get_class_name(tlobject)))
|
||||||
|
|
||||||
# Write the original .tl definition,
|
# Write the original .tl definition,
|
||||||
|
@ -264,7 +269,7 @@ class TLGenerator:
|
||||||
builder.write(' Must be a list.'.format(arg.name))
|
builder.write(' Must be a list.'.format(arg.name))
|
||||||
|
|
||||||
if arg.is_generic:
|
if arg.is_generic:
|
||||||
builder.write(' Must be another MTProtoRequest.')
|
builder.write(' Must be another TLObject request.')
|
||||||
|
|
||||||
builder.writeln()
|
builder.writeln()
|
||||||
|
|
||||||
|
@ -296,7 +301,7 @@ class TLGenerator:
|
||||||
if tlobject.is_function:
|
if tlobject.is_function:
|
||||||
builder.writeln('self.result = None')
|
builder.writeln('self.result = None')
|
||||||
builder.writeln(
|
builder.writeln(
|
||||||
'self.confirmed = True # Confirmed by default')
|
'self.content_related = True')
|
||||||
|
|
||||||
# Set the arguments
|
# Set the arguments
|
||||||
if args:
|
if args:
|
||||||
|
@ -317,10 +322,15 @@ class TLGenerator:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Cannot infer a value for ', arg)
|
raise ValueError('Cannot infer a value for ', arg)
|
||||||
|
|
||||||
|
# Well-known cases, auto-cast it to the right type
|
||||||
elif arg.type == 'InputPeer' and tlobject.is_function:
|
elif arg.type == 'InputPeer' and tlobject.is_function:
|
||||||
# Well-known case, auto-cast it to the right type
|
TLGenerator.write_get_input(builder, arg, 'get_input_peer')
|
||||||
builder.writeln(
|
elif arg.type == 'InputChannel' and tlobject.is_function:
|
||||||
'self.{0} = get_input_peer({0})'.format(arg.name))
|
TLGenerator.write_get_input(builder, arg, 'get_input_channel')
|
||||||
|
elif arg.type == 'InputUser' and tlobject.is_function:
|
||||||
|
TLGenerator.write_get_input(builder, arg, 'get_input_user')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
builder.writeln('self.{0} = {0}'.format(arg.name))
|
builder.writeln('self.{0} = {0}'.format(arg.name))
|
||||||
|
|
||||||
|
@ -413,9 +423,28 @@ class TLGenerator:
|
||||||
builder.end_block()
|
builder.end_block()
|
||||||
|
|
||||||
builder.writeln('def __str__(self):')
|
builder.writeln('def __str__(self):')
|
||||||
builder.writeln('return {}'.format(str(tlobject)))
|
builder.writeln('return TLObject.pretty_format(self)')
|
||||||
|
builder.end_block()
|
||||||
|
|
||||||
|
builder.writeln('def stringify(self):')
|
||||||
|
builder.writeln('return TLObject.pretty_format(self, indent=0)')
|
||||||
# builder.end_block() # No need to end the last block
|
# builder.end_block() # No need to end the last block
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_get_input(builder, arg, get_input_code):
|
||||||
|
"""Returns "True" if the get_input_* code was written when assigning
|
||||||
|
a parameter upon creating the request. Returns False otherwise
|
||||||
|
"""
|
||||||
|
if arg.is_vector:
|
||||||
|
builder.writeln(
|
||||||
|
'self.{0} = [{1}(_x) for _x in {0}]'
|
||||||
|
.format(arg.name, get_input_code)
|
||||||
|
)
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
builder.writeln(
|
||||||
|
'self.{0} = {1}({0})'.format(arg.name, get_input_code)
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_class_name(tlobject):
|
def get_class_name(tlobject):
|
||||||
|
@ -475,11 +504,10 @@ class TLGenerator:
|
||||||
"writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID")
|
"writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID")
|
||||||
|
|
||||||
builder.writeln('writer.write_int(len({}))'.format(name))
|
builder.writeln('writer.write_int(len({}))'.format(name))
|
||||||
builder.writeln('for {}_item in {}:'.format(arg.name, name))
|
builder.writeln('for _x in {}:'.format(name))
|
||||||
# Temporary disable .is_vector, not to enter this if again
|
# Temporary disable .is_vector, not to enter this if again
|
||||||
arg.is_vector = False
|
arg.is_vector = False
|
||||||
TLGenerator.write_onsend_code(
|
TLGenerator.write_onsend_code(builder, arg, args, name='_x')
|
||||||
builder, arg, args, name='{}_item'.format(arg.name))
|
|
||||||
arg.is_vector = True
|
arg.is_vector = True
|
||||||
|
|
||||||
elif arg.flag_indicator:
|
elif arg.flag_indicator:
|
||||||
|
@ -570,13 +598,12 @@ class TLGenerator:
|
||||||
builder.writeln("reader.read_int() # Vector's constructor ID")
|
builder.writeln("reader.read_int() # Vector's constructor ID")
|
||||||
|
|
||||||
builder.writeln('{} = [] # Initialize an empty list'.format(name))
|
builder.writeln('{} = [] # Initialize an empty list'.format(name))
|
||||||
builder.writeln('{}_len = reader.read_int()'.format(arg.name))
|
builder.writeln('_len = reader.read_int()')
|
||||||
builder.writeln('for _ in range({}_len):'.format(arg.name))
|
builder.writeln('for _ in range(_len):')
|
||||||
# Temporary disable .is_vector, not to enter this if again
|
# Temporary disable .is_vector, not to enter this if again
|
||||||
arg.is_vector = False
|
arg.is_vector = False
|
||||||
TLGenerator.write_onresponse_code(
|
TLGenerator.write_onresponse_code(builder, arg, args, name='_x')
|
||||||
builder, arg, args, name='{}_item'.format(arg.name))
|
builder.writeln('{}.append(_x)'.format(name))
|
||||||
builder.writeln('{}.append({}_item)'.format(name, arg.name))
|
|
||||||
arg.is_vector = True
|
arg.is_vector = True
|
||||||
|
|
||||||
elif arg.flag_indicator:
|
elif arg.flag_indicator:
|
||||||
|
@ -591,12 +618,14 @@ class TLGenerator:
|
||||||
builder.writeln('{} = reader.read_long()'.format(name))
|
builder.writeln('{} = reader.read_long()'.format(name))
|
||||||
|
|
||||||
elif 'int128' == arg.type:
|
elif 'int128' == arg.type:
|
||||||
builder.writeln('{} = reader.read_large_int(bits=128)'.format(
|
builder.writeln(
|
||||||
name))
|
'{} = reader.read_large_int(bits=128)'.format(name)
|
||||||
|
)
|
||||||
|
|
||||||
elif 'int256' == arg.type:
|
elif 'int256' == arg.type:
|
||||||
builder.writeln('{} = reader.read_large_int(bits=256)'.format(
|
builder.writeln(
|
||||||
name))
|
'{} = reader.read_large_int(bits=256)'.format(name)
|
||||||
|
)
|
||||||
|
|
||||||
elif 'double' == arg.type:
|
elif 'double' == arg.type:
|
||||||
builder.writeln('{} = reader.read_double()'.format(name))
|
builder.writeln('{} = reader.read_double()'.format(name))
|
||||||
|
@ -658,13 +687,3 @@ class TLGenerator:
|
||||||
builder.writeln('self.result = reader.tgread_vector()')
|
builder.writeln('self.result = reader.tgread_vector()')
|
||||||
else:
|
else:
|
||||||
builder.writeln('self.result = reader.tgread_object()')
|
builder.writeln('self.result = reader.tgread_object()')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if TLGenerator.tlobjects_exist():
|
|
||||||
print('Detected previous TLObjects. Cleaning...')
|
|
||||||
TLGenerator.clean_tlobjects()
|
|
||||||
|
|
||||||
print('Generating TLObjects...')
|
|
||||||
TLGenerator.generate_tlobjects('scheme.tl')
|
|
||||||
print('Done.')
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user