mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-05 12:40:22 +03:00
merge upstream
This commit is contained in:
commit
05e7e2aa05
35
README.rst
35
README.rst
|
@ -5,14 +5,24 @@ Telethon
|
|||
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|
||||
|
||||
**Telethon** is Telegram client implementation in **Python 3** which uses
|
||||
the latest available API of Telegram. Remember to use **pip3** to install!
|
||||
the latest available API of Telegram.
|
||||
|
||||
|
||||
What is this?
|
||||
-------------
|
||||
|
||||
Telegram is a popular messaging application. This library is meant
|
||||
to make it easy for you to write Python programs that can interact
|
||||
with Telegram. Think of it as a wrapper that has already done the
|
||||
heavy job for you, so you can focus on developing an application.
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
.. code:: sh
|
||||
|
||||
pip install telethon
|
||||
pip3 install telethon
|
||||
|
||||
|
||||
Creating a client
|
||||
|
@ -26,14 +36,9 @@ Creating a client
|
|||
# api_hash from https://my.telegram.org, under API Development.
|
||||
api_id = 12345
|
||||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
phone = '+34600000000'
|
||||
|
||||
client = TelegramClient('session_name', api_id, api_hash)
|
||||
client.connect()
|
||||
|
||||
# If you already have a previous 'session_name.session' file, skip this.
|
||||
client.sign_in(phone=phone)
|
||||
me = client.sign_in(code=77777) # Put whatever code you received here.
|
||||
client.start()
|
||||
|
||||
|
||||
Doing stuff
|
||||
|
@ -41,20 +46,20 @@ Doing stuff
|
|||
|
||||
.. code:: python
|
||||
|
||||
print(me.stringify())
|
||||
print(client.get_me().stringify())
|
||||
|
||||
client.send_message('username', 'Hello! Talking to you from Telethon')
|
||||
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
|
||||
|
||||
client.download_profile_photo(me)
|
||||
total, messages, senders = client.get_message_history('username')
|
||||
client.download_profile_photo('me')
|
||||
messages = client.get_message_history('username')
|
||||
client.download_media(messages[0])
|
||||
|
||||
|
||||
Next steps
|
||||
----------
|
||||
|
||||
Do you like how Telethon looks? Check the
|
||||
`wiki over GitHub <https://github.com/LonamiWebs/Telethon/wiki>`_ for a
|
||||
more in-depth explanation, with examples, troubleshooting issues, and more
|
||||
useful information.
|
||||
Do you like how Telethon looks? Check out
|
||||
`Read The Docs <http://telethon.rtfd.io/>`_
|
||||
for a more in-depth explanation, with examples,
|
||||
troubleshooting issues, and more useful information.
|
||||
|
|
|
@ -28,6 +28,7 @@ class DocsWriter:
|
|||
self.table_columns = 0
|
||||
self.table_columns_left = None
|
||||
self.write_copy_script = False
|
||||
self._script = ''
|
||||
|
||||
# High level writing
|
||||
def write_head(self, title, relative_css_path):
|
||||
|
@ -90,7 +91,7 @@ class DocsWriter:
|
|||
def end_menu(self):
|
||||
"""Ends an opened menu"""
|
||||
if not self.menu_began:
|
||||
raise ValueError('No menu had been started in the first place.')
|
||||
raise RuntimeError('No menu had been started in the first place.')
|
||||
self.write('</ul>')
|
||||
|
||||
def write_title(self, title, level=1):
|
||||
|
@ -254,6 +255,12 @@ class DocsWriter:
|
|||
self.write('<button onclick="cp(\'{}\');">{}</button>'
|
||||
.format(text_to_copy, text))
|
||||
|
||||
def add_script(self, src='', relative_src=None):
|
||||
if relative_src:
|
||||
self._script += '<script src="{}"></script>'.format(relative_src)
|
||||
elif src:
|
||||
self._script += '<script>{}</script>'.format(src)
|
||||
|
||||
def end_body(self):
|
||||
"""Ends the whole document. This should be called the last"""
|
||||
if self.write_copy_script:
|
||||
|
@ -268,7 +275,9 @@ class DocsWriter:
|
|||
'catch(e){}}'
|
||||
'</script>')
|
||||
|
||||
self.write('</div></body></html>')
|
||||
self.write('</div>')
|
||||
self.write(self._script)
|
||||
self.write('</body></html>')
|
||||
|
||||
# "Low" level writing
|
||||
def write(self, s):
|
||||
|
|
|
@ -207,6 +207,13 @@ def get_description(arg):
|
|||
desc.append('This argument can be omitted.')
|
||||
otherwise = True
|
||||
|
||||
if arg.type in {'InputPeer', 'InputUser', 'InputChannel'}:
|
||||
desc.append(
|
||||
'Anything entity-like will work if the library can find its '
|
||||
'<code>Input</code> version (e.g., usernames, <code>Peer</code>, '
|
||||
'<code>User</code> or <code>Channel</code> objects, etc.).'
|
||||
)
|
||||
|
||||
if arg.is_vector:
|
||||
if arg.is_generic:
|
||||
desc.append('A list of other Requests must be supplied.')
|
||||
|
@ -221,7 +228,21 @@ def get_description(arg):
|
|||
desc.insert(1, 'Otherwise,')
|
||||
desc[-1] = desc[-1][:1].lower() + desc[-1][1:]
|
||||
|
||||
return ' '.join(desc)
|
||||
return ' '.join(desc).replace(
|
||||
'list',
|
||||
'<span class="tooltip" title="Any iterable that supports len() '
|
||||
'will work too">list</span>'
|
||||
)
|
||||
|
||||
|
||||
def copy_replace(src, dst, replacements):
|
||||
"""Copies the src file into dst applying the replacements dict"""
|
||||
with open(src) as infile, open(dst, 'w') as outfile:
|
||||
outfile.write(re.sub(
|
||||
'|'.join(re.escape(k) for k in replacements),
|
||||
lambda m: str(replacements[m.group(0)]),
|
||||
infile.read()
|
||||
))
|
||||
|
||||
|
||||
def generate_documentation(scheme_file):
|
||||
|
@ -231,6 +252,7 @@ def generate_documentation(scheme_file):
|
|||
original_paths = {
|
||||
'css': 'css/docs.css',
|
||||
'arrow': 'img/arrow.svg',
|
||||
'search.js': 'js/search.js',
|
||||
'404': '404.html',
|
||||
'index_all': 'index.html',
|
||||
'index_types': 'types/index.html',
|
||||
|
@ -366,6 +388,10 @@ def generate_documentation(scheme_file):
|
|||
else:
|
||||
docs.write_text('This type has no members.')
|
||||
|
||||
# TODO Bit hacky, make everything like this? (prepending '../')
|
||||
depth = '../' * (2 if tlobject.namespace else 1)
|
||||
docs.add_script(src='prependPath = "{}";'.format(depth))
|
||||
docs.add_script(relative_src=paths['search.js'])
|
||||
docs.end_body()
|
||||
|
||||
# Find all the available types (which are not the same as the constructors)
|
||||
|
@ -540,36 +566,31 @@ def generate_documentation(scheme_file):
|
|||
type_urls = fmt(types, get_path_for_type)
|
||||
constructor_urls = fmt(constructors, get_create_path_for)
|
||||
|
||||
replace_dict = {
|
||||
'type_count': len(types),
|
||||
'method_count': len(methods),
|
||||
'constructor_count': len(tlobjects) - len(methods),
|
||||
'layer': layer,
|
||||
|
||||
'request_names': request_names,
|
||||
'type_names': type_names,
|
||||
'constructor_names': constructor_names,
|
||||
'request_urls': request_urls,
|
||||
'type_urls': type_urls,
|
||||
'constructor_urls': constructor_urls
|
||||
}
|
||||
|
||||
shutil.copy('../res/404.html', original_paths['404'])
|
||||
|
||||
with open('../res/core.html') as infile,\
|
||||
open(original_paths['index_all'], 'w') as outfile:
|
||||
text = infile.read()
|
||||
for key, value in replace_dict.items():
|
||||
text = text.replace('{' + key + '}', str(value))
|
||||
|
||||
outfile.write(text)
|
||||
copy_replace('../res/core.html', original_paths['index_all'], {
|
||||
'{type_count}': len(types),
|
||||
'{method_count}': len(methods),
|
||||
'{constructor_count}': len(tlobjects) - len(methods),
|
||||
'{layer}': layer,
|
||||
})
|
||||
os.makedirs(os.path.abspath(os.path.join(
|
||||
original_paths['search.js'], os.path.pardir
|
||||
)), exist_ok=True)
|
||||
copy_replace('../res/js/search.js', original_paths['search.js'], {
|
||||
'{request_names}': request_names,
|
||||
'{type_names}': type_names,
|
||||
'{constructor_names}': constructor_names,
|
||||
'{request_urls}': request_urls,
|
||||
'{type_urls}': type_urls,
|
||||
'{constructor_urls}': constructor_urls
|
||||
})
|
||||
|
||||
# Everything done
|
||||
print('Documentation generated.')
|
||||
|
||||
|
||||
def copy_resources():
|
||||
for d in ['css', 'img']:
|
||||
for d in ('css', 'img'):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
shutil.copy('../res/img/arrow.svg', 'img')
|
||||
|
|
|
@ -14,29 +14,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="main_div">
|
||||
<!-- You can append '?q=query' to the URL to default to a search -->
|
||||
<input id="searchBox" type="text" onkeyup="updateSearch()"
|
||||
placeholder="Search for requests and types…" />
|
||||
|
||||
<div id="searchDiv">
|
||||
|
||||
<details open><summary class="title">Methods (<span id="methodsCount">0</span>)</summary>
|
||||
<ul id="methodsList" class="together">
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details open><summary class="title">Types (<span id="typesCount">0</span>)</summary>
|
||||
<ul id="typesList" class="together">
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details><summary class="title">Constructors (<span id="constructorsCount">0</span>)</summary>
|
||||
<ul id="constructorsList" class="together">
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div id="contentDiv">
|
||||
<noscript>Please enable JavaScript if you would like to use search.</noscript>
|
||||
<h1>Telethon API</h1>
|
||||
<p>This documentation was generated straight from the <code>scheme.tl</code>
|
||||
provided by Telegram. However, there is no official documentation per se
|
||||
|
@ -44,8 +22,15 @@
|
|||
page aims to provide easy access to all the available methods, their
|
||||
definition and parameters.</p>
|
||||
|
||||
<p>Although this documentation was generated for <i>Telethon</i>, it may
|
||||
be useful for any other Telegram library out there.</p>
|
||||
<p>Please note that when you see this:</p>
|
||||
<pre>---functions---
|
||||
users.getUsers#0d91a548 id:Vector<InputUser> = Vector<User></pre>
|
||||
|
||||
<p>This is <b>not</b> Python code. It's the "TL definition". It's
|
||||
an easy-to-read line that gives a quick overview on the parameters
|
||||
and its result. You don't need to worry about this. See
|
||||
<a href="http://telethon.readthedocs.io/en/latest/extra/developing/understanding-the-type-language.html">here</a>
|
||||
for more details on it.</p>
|
||||
|
||||
<h3>Index</h3>
|
||||
<ul>
|
||||
|
@ -69,12 +54,12 @@
|
|||
<p>Currently there are <b>{method_count} methods</b> available for the layer
|
||||
{layer}. The complete list can be seen <a href="methods/index.html">here</a>.
|
||||
<br /><br />
|
||||
Methods, also known as <i>requests</i>, are used to interact with
|
||||
the Telegram API itself and are invoked with a call to <code>.invoke()</code>.
|
||||
<b>Only these</b> can be passed to <code>.invoke()</code>! You cannot
|
||||
<code>.invoke()</code> types or constructors, only requests. After this,
|
||||
Telegram will return a <code>result</code>, which may be, for instance,
|
||||
a bunch of messages, some dialogs, users, etc.</p>
|
||||
Methods, also known as <i>requests</i>, are used to interact with the
|
||||
Telegram API itself and are invoked through <code>client(Request(...))</code>.
|
||||
<b>Only these</b> can be used like that! You cannot invoke types or
|
||||
constructors, only requests. After this, Telegram will return a
|
||||
<code>result</code>, which may be, for instance, a bunch of messages,
|
||||
some dialogs, users, etc.</p>
|
||||
|
||||
<h3 id="types">Types</h3>
|
||||
<p>Currently there are <b>{type_count} types</b>. You can see the full
|
||||
|
@ -145,170 +130,20 @@
|
|||
</li>
|
||||
<li id="date"><b>date</b>:
|
||||
Although this type is internally used as an <code>int</code>,
|
||||
you can pass a <code>datetime</code> object instead to work
|
||||
with date parameters.
|
||||
you can pass a <code>datetime</code> or <code>date</code> object
|
||||
instead to work with date parameters.<br />
|
||||
Note that the library uses the date in <b>UTC+0</b>, since timezone
|
||||
conversion is not responsibility of the library. Furthermore, this
|
||||
eases converting into any other timezone without the need for a middle
|
||||
step.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="example">Full example</h3>
|
||||
<p>The following example demonstrates:</p>
|
||||
<ol>
|
||||
<li>How to create a <code>TelegramClient</code>.</li>
|
||||
<li>Connecting to the Telegram servers and authorizing an user.</li>
|
||||
<li>Retrieving a list of chats (<i>dialogs</i>).</li>
|
||||
<li>Invoking a request without the built-in methods.</li>
|
||||
</ol>
|
||||
<pre><span class="sh3">#!/usr/bin/python3</span>
|
||||
<span class="sh4">from</span> telethon <span class="sh4">import</span> TelegramClient
|
||||
<span class="sh4">from</span> telethon.tl.functions.messages <span class="sh4">import</span> GetHistoryRequest
|
||||
|
||||
<span class="sh3"># <b>(1)</b> Use your own values here</span>
|
||||
api_id = <span class="sh1">12345</span>
|
||||
api_hash = <span class="sh2">'0123456789abcdef0123456789abcdef'</span>
|
||||
phone = <span class="sh2">'+34600000000'</span>
|
||||
|
||||
<span class="sh3"># <b>(2)</b> Create the client and connect</span>
|
||||
client = TelegramClient(<span class="sh2">'username'</span>, api_id, api_hash)
|
||||
client.connect()
|
||||
|
||||
<span class="sh3"># Ensure you're authorized</span>
|
||||
if not client.is_user_authorized():
|
||||
client.send_code_request(phone)
|
||||
client.sign_in(phone, input(<span class="sh2">'Enter the code: '</span>))
|
||||
|
||||
<span class="sh3"># <b>(3)</b> Using built-in methods</span>
|
||||
dialogs, entities = client.get_dialogs(<span class="sh1">10</span>)
|
||||
entity = entities[<span class="sh1">0</span>]
|
||||
|
||||
<span class="sh3"># <b>(4)</b> !! Invoking a request manually !!</span>
|
||||
result = <b>client</b>(GetHistoryRequest(
|
||||
entity,
|
||||
limit=<span class="sh1">20</span>,
|
||||
offset_date=<span class="sh1">None</span>,
|
||||
offset_id=<span class="sh1">0</span>,
|
||||
max_id=<span class="sh1">0</span>,
|
||||
min_id=<span class="sh1">0</span>,
|
||||
add_offset=<span class="sh1">0</span>
|
||||
))
|
||||
|
||||
<span class="sh3"># Now you have access to the first 20 messages</span>
|
||||
messages = result.messages</pre>
|
||||
|
||||
<p>As it can be seen, manually calling requests with
|
||||
<code>client(request)</code> (or using the old way, by calling
|
||||
<code>client.invoke(request)</code>) is way more verbose than using the
|
||||
built-in methods (such as <code>client.get_dialogs()</code>).</p>
|
||||
|
||||
<p>However, and
|
||||
given that there are so many methods available, it's impossible to provide
|
||||
a nice interface to things that may change over time. To get full access,
|
||||
however, you're still able to invoke these methods manually.</p>
|
||||
<p>Documentation for this is now
|
||||
<a href="http://telethon.readthedocs.io/en/latest/extra/advanced-usage/accessing-the-full-api.html">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
contentDiv = document.getElementById("contentDiv");
|
||||
searchDiv = document.getElementById("searchDiv");
|
||||
searchBox = document.getElementById("searchBox");
|
||||
|
||||
// Search lists
|
||||
methodsList = document.getElementById("methodsList");
|
||||
methodsCount = document.getElementById("methodsCount");
|
||||
|
||||
typesList = document.getElementById("typesList");
|
||||
typesCount = document.getElementById("typesCount");
|
||||
|
||||
constructorsList = document.getElementById("constructorsList");
|
||||
constructorsCount = document.getElementById("constructorsCount");
|
||||
|
||||
try {
|
||||
requests = [{request_names}];
|
||||
types = [{type_names}];
|
||||
constructors = [{constructor_names}];
|
||||
|
||||
requestsu = [{request_urls}];
|
||||
typesu = [{type_urls}];
|
||||
constructorsu = [{constructor_urls}];
|
||||
} catch (e) {
|
||||
requests = [];
|
||||
types = [];
|
||||
constructors = [];
|
||||
requestsu = [];
|
||||
typesu = [];
|
||||
constructorsu = [];
|
||||
}
|
||||
|
||||
// Given two input arrays "original" and "original urls" and a query,
|
||||
// return a pair of arrays with matching "query" elements from "original".
|
||||
//
|
||||
// TODO Perhaps return an array of pairs instead a pair of arrays (for cache).
|
||||
function getSearchArray(original, originalu, query) {
|
||||
var destination = [];
|
||||
var destinationu = [];
|
||||
|
||||
for (var i = 0; i < original.length; ++i) {
|
||||
if (original[i].toLowerCase().indexOf(query) != -1) {
|
||||
destination.push(original[i]);
|
||||
destinationu.push(originalu[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return [destination, destinationu];
|
||||
}
|
||||
|
||||
// Modify "countSpan" and "resultList" accordingly based on the elements
|
||||
// given as [[elements], [element urls]] (both with the same length)
|
||||
function buildList(countSpan, resultList, foundElements) {
|
||||
var result = "";
|
||||
for (var i = 0; i < foundElements[0].length; ++i) {
|
||||
result += '<li>';
|
||||
result += '<a href="' + foundElements[1][i] + '">';
|
||||
result += foundElements[0][i];
|
||||
result += '</a></li>';
|
||||
}
|
||||
|
||||
countSpan.innerHTML = "" + foundElements[0].length;
|
||||
resultList.innerHTML = result;
|
||||
}
|
||||
|
||||
function updateSearch() {
|
||||
if (searchBox.value) {
|
||||
contentDiv.style.display = "none";
|
||||
searchDiv.style.display = "";
|
||||
|
||||
var query = searchBox.value.toLowerCase();
|
||||
|
||||
var foundRequests = getSearchArray(requests, requestsu, query);
|
||||
var foundTypes = getSearchArray(types, typesu, query);
|
||||
var foundConstructors = getSearchArray(
|
||||
constructors, constructorsu, query
|
||||
);
|
||||
|
||||
buildList(methodsCount, methodsList, foundRequests);
|
||||
buildList(typesCount, typesList, foundTypes);
|
||||
buildList(constructorsCount, constructorsList, foundConstructors);
|
||||
} else {
|
||||
contentDiv.style.display = "";
|
||||
searchDiv.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function getQuery(name) {
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i = 0; i != vars.length; ++i) {
|
||||
var pair = vars[i].split("=");
|
||||
if (pair[0] == name)
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
|
||||
var query = getQuery('q');
|
||||
if (query) {
|
||||
searchBox.value = query;
|
||||
}
|
||||
|
||||
updateSearch();
|
||||
</script>
|
||||
<script src="js/search.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -108,6 +108,10 @@ span.sh4 {
|
|||
color: #06c;
|
||||
}
|
||||
|
||||
span.tooltip {
|
||||
border-bottom: 1px dashed #444;
|
||||
}
|
||||
|
||||
#searchBox {
|
||||
width: 100%;
|
||||
border: none;
|
||||
|
|
208
docs/res/js/search.js
Normal file
208
docs/res/js/search.js
Normal file
|
@ -0,0 +1,208 @@
|
|||
root = document.getElementById("main_div");
|
||||
root.innerHTML = `
|
||||
<!-- You can append '?q=query' to the URL to default to a search -->
|
||||
<input id="searchBox" type="text" onkeyup="updateSearch()"
|
||||
placeholder="Search for requests and types…" />
|
||||
|
||||
<div id="searchDiv">
|
||||
<div id="exactMatch" style="display:none;">
|
||||
<b>Exact match:</b>
|
||||
<ul id="exactList" class="together">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<details open><summary class="title">Methods (<span id="methodsCount">0</span>)</summary>
|
||||
<ul id="methodsList" class="together">
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details open><summary class="title">Types (<span id="typesCount">0</span>)</summary>
|
||||
<ul id="typesList" class="together">
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details><summary class="title">Constructors (<span id="constructorsCount">0</span>)</summary>
|
||||
<ul id="constructorsList" class="together">
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
<div id="contentDiv">
|
||||
` + root.innerHTML + "</div>";
|
||||
|
||||
// HTML modified, now load documents
|
||||
contentDiv = document.getElementById("contentDiv");
|
||||
searchDiv = document.getElementById("searchDiv");
|
||||
searchBox = document.getElementById("searchBox");
|
||||
|
||||
// Search lists
|
||||
methodsList = document.getElementById("methodsList");
|
||||
methodsCount = document.getElementById("methodsCount");
|
||||
|
||||
typesList = document.getElementById("typesList");
|
||||
typesCount = document.getElementById("typesCount");
|
||||
|
||||
constructorsList = document.getElementById("constructorsList");
|
||||
constructorsCount = document.getElementById("constructorsCount");
|
||||
|
||||
// Exact match
|
||||
exactMatch = document.getElementById("exactMatch");
|
||||
exactList = document.getElementById("exactList");
|
||||
|
||||
try {
|
||||
requests = [{request_names}];
|
||||
types = [{type_names}];
|
||||
constructors = [{constructor_names}];
|
||||
|
||||
requestsu = [{request_urls}];
|
||||
typesu = [{type_urls}];
|
||||
constructorsu = [{constructor_urls}];
|
||||
} catch (e) {
|
||||
requests = [];
|
||||
types = [];
|
||||
constructors = [];
|
||||
requestsu = [];
|
||||
typesu = [];
|
||||
constructorsu = [];
|
||||
}
|
||||
|
||||
if (typeof prependPath !== 'undefined') {
|
||||
for (var i = 0; i != requestsu.length; ++i) {
|
||||
requestsu[i] = prependPath + requestsu[i];
|
||||
}
|
||||
for (var i = 0; i != typesu.length; ++i) {
|
||||
typesu[i] = prependPath + typesu[i];
|
||||
}
|
||||
for (var i = 0; i != constructorsu.length; ++i) {
|
||||
constructorsu[i] = prependPath + constructorsu[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Assumes haystack has no whitespace and both are lowercase.
|
||||
function find(haystack, needle) {
|
||||
if (needle.length == 0) {
|
||||
return true;
|
||||
}
|
||||
var hi = 0;
|
||||
var ni = 0;
|
||||
while (true) {
|
||||
while (needle[ni] < 'a' || needle[ni] > 'z') {
|
||||
++ni;
|
||||
if (ni == needle.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
while (haystack[hi] != needle[ni]) {
|
||||
++hi;
|
||||
if (hi == haystack.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
++hi;
|
||||
++ni;
|
||||
if (ni == needle.length) {
|
||||
return true;
|
||||
}
|
||||
if (hi == haystack.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Given two input arrays "original" and "original urls" and a query,
|
||||
// return a pair of arrays with matching "query" elements from "original".
|
||||
//
|
||||
// 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 (find(original[i].toLowerCase(), query)) {
|
||||
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>';
|
||||
}
|
||||
|
||||
if (countSpan) {
|
||||
countSpan.innerHTML = "" + foundElements[0].length;
|
||||
}
|
||||
resultList.innerHTML = result;
|
||||
}
|
||||
|
||||
function updateSearch() {
|
||||
if (searchBox.value) {
|
||||
contentDiv.style.display = "none";
|
||||
searchDiv.style.display = "";
|
||||
|
||||
var query = searchBox.value.toLowerCase();
|
||||
|
||||
var foundRequests = getSearchArray(requests, requestsu, query);
|
||||
var foundTypes = getSearchArray(types, typesu, query);
|
||||
var foundConstructors = getSearchArray(
|
||||
constructors, constructorsu, query
|
||||
);
|
||||
|
||||
buildList(methodsCount, methodsList, foundRequests);
|
||||
buildList(typesCount, typesList, foundTypes);
|
||||
buildList(constructorsCount, constructorsList, foundConstructors);
|
||||
|
||||
// Now look for exact matches
|
||||
var original = requests.concat(constructors);
|
||||
var originalu = requestsu.concat(constructorsu);
|
||||
var destination = [];
|
||||
var destinationu = [];
|
||||
|
||||
for (var i = 0; i < original.length; ++i) {
|
||||
if (original[i].toLowerCase().replace("request", "") == query) {
|
||||
destination.push(original[i]);
|
||||
destinationu.push(originalu[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (destination.length == 0) {
|
||||
exactMatch.style.display = "none";
|
||||
} else {
|
||||
exactMatch.style.display = "";
|
||||
buildList(null, exactList, [destination, destinationu]);
|
||||
return destinationu[0];
|
||||
}
|
||||
} else {
|
||||
contentDiv.style.display = "";
|
||||
searchDiv.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function getQuery(name) {
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i = 0; i != vars.length; ++i) {
|
||||
var pair = vars[i].split("=");
|
||||
if (pair[0] == name)
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
|
||||
var query = getQuery('q');
|
||||
if (query) {
|
||||
searchBox.value = query;
|
||||
}
|
||||
|
||||
var exactUrl = updateSearch();
|
||||
var redirect = getQuery('redirect');
|
||||
if (exactUrl && redirect != 'no') {
|
||||
window.location = exactUrl;
|
||||
}
|
3
optional-requirements.txt
Normal file
3
optional-requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
cryptg
|
||||
pysocks
|
||||
hachoir3
|
|
@ -17,9 +17,15 @@
|
|||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
|
||||
|
||||
tl_ref_url = 'https://lonamiwebs.github.io/Telethon'
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
@ -31,7 +37,13 @@
|
|||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc']
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'custom_roles'
|
||||
]
|
||||
|
||||
# Change the default role so we can avoid prefixing everything with :obj:
|
||||
default_role = "py:obj"
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
@ -55,9 +67,12 @@ author = 'Lonami'
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.15'
|
||||
with open(os.path.join(root, 'telethon', 'version.py')) as f:
|
||||
version = re.search(r"^__version__\s+=\s+'(.*)'$",
|
||||
f.read(), flags=re.MULTILINE).group(1)
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.15.5'
|
||||
release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -145,7 +160,7 @@ latex_elements = {
|
|||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Telethon.tex', 'Telethon Documentation',
|
||||
'Jeff', 'manual'),
|
||||
author, 'manual'),
|
||||
]
|
||||
|
||||
|
||||
|
|
69
readthedocs/custom_roles.py
Normal file
69
readthedocs/custom_roles.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from docutils import nodes, utils
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
|
||||
|
||||
def make_link_node(rawtext, app, name, options):
|
||||
"""
|
||||
Create a link to the TL reference.
|
||||
|
||||
:param rawtext: Text being replaced with link node.
|
||||
:param app: Sphinx application context
|
||||
:param name: Name of the object to link to
|
||||
:param options: Options dictionary passed to role func.
|
||||
"""
|
||||
try:
|
||||
base = app.config.tl_ref_url
|
||||
if not base:
|
||||
raise AttributeError
|
||||
except AttributeError as e:
|
||||
raise ValueError('tl_ref_url config value is not set') from e
|
||||
|
||||
if base[-1] != '/':
|
||||
base += '/'
|
||||
|
||||
set_classes(options)
|
||||
node = nodes.reference(rawtext, utils.unescape(name),
|
||||
refuri='{}?q={}'.format(base, name),
|
||||
**options)
|
||||
return node
|
||||
|
||||
|
||||
def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None):
|
||||
"""
|
||||
Link to the TL reference.
|
||||
|
||||
Returns 2 part tuple containing list of nodes to insert into the
|
||||
document and a list of system messages. Both are allowed to be empty.
|
||||
|
||||
:param name: The role name used in the document.
|
||||
:param rawtext: The entire markup snippet, with role.
|
||||
:param text: The text marked with the role.
|
||||
:param lineno: The line number where rawtext appears in the input.
|
||||
:param inliner: The inliner instance that called us.
|
||||
:param options: Directive options for customization.
|
||||
:param content: The directive content for customization.
|
||||
"""
|
||||
if options is None:
|
||||
options = {}
|
||||
if content is None:
|
||||
content = []
|
||||
|
||||
# TODO Report error on type not found?
|
||||
# Usage:
|
||||
# msg = inliner.reporter.error(..., line=lineno)
|
||||
# return [inliner.problematic(rawtext, rawtext, msg)], [msg]
|
||||
app = inliner.document.settings.env.app
|
||||
node = make_link_node(rawtext, app, text, options)
|
||||
return [node], []
|
||||
|
||||
|
||||
def setup(app):
|
||||
"""
|
||||
Install the plugin.
|
||||
|
||||
:param app: Sphinx application context.
|
||||
"""
|
||||
app.info('Initializing TL reference plugin')
|
||||
app.add_role('tl', tl_role)
|
||||
app.add_config_value('tl_ref_url', None, 'env')
|
||||
return
|
140
readthedocs/extra/advanced-usage/accessing-the-full-api.rst
Normal file
140
readthedocs/extra/advanced-usage/accessing-the-full-api.rst
Normal file
|
@ -0,0 +1,140 @@
|
|||
.. _accessing-the-full-api:
|
||||
|
||||
======================
|
||||
Accessing the Full API
|
||||
======================
|
||||
|
||||
|
||||
The ``TelegramClient`` doesn't offer a method for every single request
|
||||
the Telegram API supports. However, it's very simple to *call* or *invoke*
|
||||
any request. Whenever you need something, don't forget to `check the
|
||||
documentation`__ and look for the `method you need`__. There you can go
|
||||
through a sorted list of everything you can do.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The reason to keep both https://lonamiwebs.github.io/Telethon and this
|
||||
documentation alive is that the former allows instant search results
|
||||
as you type, and a "Copy import" button. If you like namespaces, you
|
||||
can also do ``from telethon.tl import types, functions``. Both work.
|
||||
|
||||
|
||||
You should also refer to the documentation to see what the objects
|
||||
(constructors) Telegram returns look like. Every constructor inherits
|
||||
from a common type, and that's the reason for this distinction.
|
||||
|
||||
Say ``client.send_message()`` didn't exist, we could use the `search`__
|
||||
to look for "message". There we would find :tl:`SendMessageRequest`,
|
||||
which we can work with.
|
||||
|
||||
Every request is a Python class, and has the parameters needed for you
|
||||
to invoke it. You can also call ``help(request)`` for information on
|
||||
what input parameters it takes. Remember to "Copy import to the
|
||||
clipboard", or your script won't be aware of this class! Now we have:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import SendMessageRequest
|
||||
|
||||
If you're going to use a lot of these, you may do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl import types, functions
|
||||
# We now have access to 'functions.messages.SendMessageRequest'
|
||||
|
||||
We see that this request must take at least two parameters, a ``peer``
|
||||
of type :tl:`InputPeer`, and a ``message`` which is just a Python
|
||||
``str``\ ing.
|
||||
|
||||
How can we retrieve this :tl:`InputPeer`? We have two options. We manually
|
||||
construct one, for instance:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.types import InputPeerUser
|
||||
|
||||
peer = InputPeerUser(user_id, user_hash)
|
||||
|
||||
Or we call ``.get_input_entity()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
peer = client.get_input_entity('someone')
|
||||
|
||||
When you're going to invoke an API method, most require you to pass an
|
||||
:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using
|
||||
``.get_input_entity()`` is more straightforward (and often
|
||||
immediate, if you've seen the user before, know their ID, etc.).
|
||||
If you also need to have information about the whole user, use
|
||||
``.get_entity()`` instead:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
entity = client.get_entity('someone')
|
||||
|
||||
In the later case, when you use the entity, the library will cast it to
|
||||
its "input" version for you. If you already have the complete user and
|
||||
want to cache its input version so the library doesn't have to do this
|
||||
every time its used, simply call ``.get_input_peer``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import utils
|
||||
peer = utils.get_input_user(entity)
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Since ``v0.16.2`` this is further simplified. The ``Request`` itself
|
||||
will call ``client.get_input_entity()`` for you when required, but
|
||||
it's good to remember what's happening.
|
||||
|
||||
|
||||
After this small parenthesis about ``.get_entity`` versus
|
||||
``.get_input_entity``, we have everything we need. To ``.invoke()`` our
|
||||
request we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
result = client(SendMessageRequest(peer, 'Hello there!'))
|
||||
# __call__ is an alias for client.invoke(request). Both will work
|
||||
|
||||
Message sent! Of course, this is only an example. There are nearly 250
|
||||
methods available as of layer 73, and you can use every single of them
|
||||
as you wish. Remember to use the right types! To sum up:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
result = client(SendMessageRequest(
|
||||
client.get_input_entity('username'), 'Hello there!'
|
||||
))
|
||||
|
||||
|
||||
This can further be simplified to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
result = client(SendMessageRequest('username', 'Hello there!'))
|
||||
# Or even
|
||||
result = client(SendMessageRequest(PeerChannel(id), 'Hello there!'))
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Note that some requests have a "hash" parameter. This is **not**
|
||||
your ``api_hash``! It likely isn't your self-user ``.access_hash`` either.
|
||||
|
||||
It's a special hash used by Telegram to only send a difference of new data
|
||||
that you don't already have with that request, so you can leave it to 0,
|
||||
and it should work (which means no hash is known yet).
|
||||
|
||||
For those requests having a "limit" parameter, you can often set it to
|
||||
zero to signify "return default amount". This won't work for all of them
|
||||
though, for instance, in "messages.search" it will actually return 0 items.
|
||||
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/index.html
|
||||
__ https://lonamiwebs.github.io/Telethon/?q=message
|
124
readthedocs/extra/advanced-usage/sessions.rst
Normal file
124
readthedocs/extra/advanced-usage/sessions.rst
Normal file
|
@ -0,0 +1,124 @@
|
|||
.. _sessions:
|
||||
|
||||
==============
|
||||
Session Files
|
||||
==============
|
||||
|
||||
The first parameter you pass to the constructor of the ``TelegramClient`` is
|
||||
the ``session``, and defaults to be the session name (or full path). That is,
|
||||
if you create a ``TelegramClient('anon')`` instance and connect, an
|
||||
``anon.session`` file will be created on the working directory.
|
||||
|
||||
These database files using ``sqlite3`` contain the required information to
|
||||
talk to the Telegram servers, such as to which IP the client should connect,
|
||||
port, authorization key so that messages can be encrypted, and so on.
|
||||
|
||||
These files will by default also save all the input entities that you've seen,
|
||||
so that you can get information about an user or channel by just their ID.
|
||||
Telegram will **not** send their ``access_hash`` required to retrieve more
|
||||
information about them, if it thinks you have already seem them. For this
|
||||
reason, the library needs to store this information offline.
|
||||
|
||||
The library will by default too save all the entities (chats and channels
|
||||
with their name and username, and users with the phone too) in the session
|
||||
file, so that you can quickly access them by username or phone number.
|
||||
|
||||
If you're not going to work with updates, or don't need to cache the
|
||||
``access_hash`` associated with the entities' ID, you can disable this
|
||||
by setting ``client.session.save_entities = False``.
|
||||
|
||||
Custom Session Storage
|
||||
----------------------
|
||||
|
||||
If you don't want to use the default SQLite session storage, you can also use
|
||||
one of the other implementations or implement your own storage.
|
||||
|
||||
To use a custom session storage, simply pass the custom session instance to
|
||||
``TelegramClient`` instead of the session name.
|
||||
|
||||
Telethon contains two implementations of the abstract ``Session`` class:
|
||||
|
||||
* ``MemorySession``: stores session data in Python variables.
|
||||
* ``SQLiteSession``, (default): stores sessions in their own SQLite databases.
|
||||
|
||||
There are other community-maintained implementations available:
|
||||
|
||||
* `SQLAlchemy <https://github.com/tulir/telethon-session-sqlalchemy>`_: stores all sessions in a single database via SQLAlchemy.
|
||||
* `Redis <https://github.com/ezdev128/telethon-session-redis>`_: stores all sessions in a single Redis data store.
|
||||
|
||||
Creating your own storage
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The easiest way to create your own storage implementation is to use ``MemorySession``
|
||||
as the base and check out how ``SQLiteSession`` or one of the community-maintained
|
||||
implementations work. You can find the relevant Python files under the ``sessions``
|
||||
directory in Telethon.
|
||||
|
||||
After you have made your own implementation, you can add it to the community-maintained
|
||||
session implementation list above with a pull request.
|
||||
|
||||
SQLite Sessions and Heroku
|
||||
--------------------------
|
||||
|
||||
You probably have a newer version of SQLite installed (>= 3.8.2). Heroku uses
|
||||
SQLite 3.7.9 which does not support ``WITHOUT ROWID``. So, if you generated
|
||||
your session file on a system with SQLite >= 3.8.2 your session file will not
|
||||
work on Heroku's platform and will throw a corrupted schema error.
|
||||
|
||||
There are multiple ways to solve this, the easiest of which is generating a
|
||||
session file on your Heroku dyno itself. The most complicated is creating
|
||||
a custom buildpack to install SQLite >= 3.8.2.
|
||||
|
||||
|
||||
Generating a SQLite Session File on a Heroku Dyno
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. note::
|
||||
Due to Heroku's ephemeral filesystem all dynamically generated
|
||||
files not part of your applications buildpack or codebase are destroyed
|
||||
upon each restart.
|
||||
|
||||
.. warning::
|
||||
Do not restart your application Dyno at any point prior to retrieving your
|
||||
session file. Constantly creating new session files from Telegram's API
|
||||
will result in a 24 hour rate limit ban.
|
||||
|
||||
Due to Heroku's ephemeral filesystem all dynamically generated
|
||||
files not part of your applications buildpack or codebase are destroyed upon
|
||||
each restart.
|
||||
|
||||
Using this scaffolded code we can start the authentication process:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient('login.session', api_id, api_hash).start()
|
||||
|
||||
At this point your Dyno will crash because you cannot access stdin. Open your
|
||||
Dyno's control panel on the Heroku website and "Run console" from the "More"
|
||||
dropdown at the top right. Enter ``bash`` and wait for it to load.
|
||||
|
||||
You will automatically be placed into your applications working directory.
|
||||
So run your application ``python app.py`` and now you can complete the input
|
||||
requests such as "what is your phone number" etc.
|
||||
|
||||
Once you're successfully authenticated exit your application script with
|
||||
CTRL + C and ``ls`` to confirm ``login.session`` exists in your current
|
||||
directory. Now you can create a git repo on your account and commit
|
||||
``login.session`` to that repo.
|
||||
|
||||
You cannot ``ssh`` into your Dyno instance because it has crashed, so unless
|
||||
you programatically upload this file to a server host this is the only way to
|
||||
get it off of your Dyno.
|
||||
|
||||
You now have a session file compatible with SQLite <= 3.8.2. Now you can
|
||||
programatically fetch this file from an external host (Firebase, S3 etc.)
|
||||
and login to your session using the following scaffolded code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
fileName, headers = urllib.request.urlretrieve(file_url, 'login.session')
|
||||
client = TelegramClient(os.path.abspath(fileName), api_id, api_hash).start()
|
||||
|
||||
.. note::
|
||||
- ``urlretrieve`` will be depreciated, consider using ``requests``.
|
||||
- ``file_url`` represents the location of your file.
|
|
@ -1,58 +0,0 @@
|
|||
=========================
|
||||
Signing In
|
||||
=========================
|
||||
|
||||
.. note::
|
||||
Make sure you have gone through :ref:`prelude` already!
|
||||
|
||||
|
||||
Two Factor Authorization (2FA)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have Two Factor Authorization (from now on, 2FA) enabled on your account, calling
|
||||
:meth:`telethon.TelegramClient.sign_in` will raise a `SessionPasswordNeededError`.
|
||||
When this happens, just :meth:`telethon.TelegramClient.sign_in` again with a ``password=``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import getpass
|
||||
from telethon.errors import SessionPasswordNeededError
|
||||
|
||||
client.sign_in(phone)
|
||||
try:
|
||||
client.sign_in(code=input('Enter code: '))
|
||||
except SessionPasswordNeededError:
|
||||
client.sign_in(password=getpass.getpass())
|
||||
|
||||
Enabling 2FA
|
||||
*************
|
||||
|
||||
If you don't have 2FA enabled, but you would like to do so through Telethon, take as example the following code snippet:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import os
|
||||
from hashlib import sha256
|
||||
from telethon.tl.functions import account
|
||||
from telethon.tl.types.account import PasswordInputSettings
|
||||
|
||||
new_salt = client(account.GetPasswordRequest()).new_salt
|
||||
salt = new_salt + os.urandom(8) # new random salt
|
||||
|
||||
pw = 'secret'.encode('utf-8') # type your new password here
|
||||
hint = 'hint'
|
||||
|
||||
pw_salted = salt + pw + salt
|
||||
pw_hash = sha256(pw_salted).digest()
|
||||
|
||||
result = client(account.UpdatePasswordSettingsRequest(
|
||||
current_password_hash=salt,
|
||||
new_settings=PasswordInputSettings(
|
||||
new_salt=salt,
|
||||
new_password_hash=pw_hash,
|
||||
hint=hint
|
||||
)
|
||||
))
|
||||
|
||||
Thanks to `Issue 259 <https://github.com/LonamiWebs/Telethon/issues/259>`_ for the tip!
|
||||
|
144
readthedocs/extra/advanced-usage/update-modes.rst
Normal file
144
readthedocs/extra/advanced-usage/update-modes.rst
Normal file
|
@ -0,0 +1,144 @@
|
|||
.. _update-modes:
|
||||
|
||||
============
|
||||
Update Modes
|
||||
============
|
||||
|
||||
|
||||
The library can run in four distinguishable modes:
|
||||
|
||||
- With no extra threads at all.
|
||||
- With an extra thread that receives everything as soon as possible (default).
|
||||
- With several worker threads that run your update handlers.
|
||||
- A mix of the above.
|
||||
|
||||
Since this section is about updates, we'll describe the simplest way to
|
||||
work with them.
|
||||
|
||||
|
||||
Using multiple workers
|
||||
**********************
|
||||
|
||||
When you create your client, simply pass a number to the
|
||||
``update_workers`` parameter:
|
||||
|
||||
``client = TelegramClient('session', api_id, api_hash, update_workers=2)``
|
||||
|
||||
You can set any amount of workers you want. The more you put, the more
|
||||
update handlers that can be called "at the same time". One or two should
|
||||
suffice most of the time, since setting more will not make things run
|
||||
faster most of the times (actually, it could slow things down).
|
||||
|
||||
The next thing you want to do is to add a method that will be called when
|
||||
an `Update`__ arrives:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def callback(update):
|
||||
print('I received', update)
|
||||
|
||||
client.add_event_handler(callback)
|
||||
# do more work here, or simply sleep!
|
||||
|
||||
That's it! This is the old way to listen for raw updates, with no further
|
||||
processing. If this feels annoying for you, remember that you can always
|
||||
use :ref:`working-with-updates` but maybe use this for some other cases.
|
||||
|
||||
Now let's do something more interesting. Every time an user talks to use,
|
||||
let's reply to them with the same text reversed:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.types import UpdateShortMessage, PeerUser
|
||||
|
||||
def replier(update):
|
||||
if isinstance(update, UpdateShortMessage) and not update.out:
|
||||
client.send_message(PeerUser(update.user_id), update.message[::-1])
|
||||
|
||||
|
||||
client.add_event_handler(replier)
|
||||
input('Press enter to stop this!')
|
||||
client.disconnect()
|
||||
|
||||
We only ask you one thing: don't keep this running for too long, or your
|
||||
contacts will go mad.
|
||||
|
||||
|
||||
Spawning no worker at all
|
||||
*************************
|
||||
|
||||
All the workers do is loop forever and poll updates from a queue that is
|
||||
filled from the ``ReadThread``, responsible for reading every item off
|
||||
the network. If you only need a worker and the ``MainThread`` would be
|
||||
doing no other job, this is the preferred way. You can easily do the same
|
||||
as the workers like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
while True:
|
||||
try:
|
||||
update = client.updates.poll()
|
||||
if not update:
|
||||
continue
|
||||
|
||||
print('I received', update)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
client.disconnect()
|
||||
|
||||
Note that ``poll`` accepts a ``timeout=`` parameter, and it will return
|
||||
``None`` if other thread got the update before you could or if the timeout
|
||||
expired, so it's important to check ``if not update``.
|
||||
|
||||
This can coexist with the rest of ``N`` workers, or you can set it to ``0``
|
||||
additional workers:
|
||||
|
||||
``client = TelegramClient('session', api_id, api_hash, update_workers=0)``
|
||||
|
||||
You **must** set it to ``0`` (or higher), as it defaults to ``None`` and that
|
||||
has a different meaning. ``None`` workers means updates won't be processed
|
||||
*at all*, so you must set it to some integer value if you want
|
||||
``client.updates.poll()`` to work.
|
||||
|
||||
|
||||
Using the main thread instead the ``ReadThread``
|
||||
************************************************
|
||||
|
||||
If you have no work to do on the ``MainThread`` and you were planning to have
|
||||
a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary
|
||||
``ReadThread`` at all like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient(
|
||||
...
|
||||
spawn_read_thread=False
|
||||
)
|
||||
|
||||
And then ``.idle()`` from the ``MainThread``:
|
||||
|
||||
``client.idle()``
|
||||
|
||||
You can stop it with :kbd:`Control+C`, and you can configure the signals
|
||||
to be used in a similar fashion to `Python Telegram Bot`__.
|
||||
|
||||
As a complete example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def callback(update):
|
||||
print('I received', update)
|
||||
|
||||
client = TelegramClient('session', api_id, api_hash,
|
||||
update_workers=1, spawn_read_thread=False)
|
||||
|
||||
client.connect()
|
||||
client.add_event_handler(callback)
|
||||
client.idle() # ends with Ctrl+C
|
||||
|
||||
|
||||
This is the preferred way to use if you're simply going to listen for updates.
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/types/update.html
|
||||
__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460
|
|
@ -1,324 +0,0 @@
|
|||
=========================
|
||||
Users and Chats
|
||||
=========================
|
||||
|
||||
.. note::
|
||||
Make sure you have gone through :ref:`prelude` already!
|
||||
|
||||
.. contents::
|
||||
:depth: 2
|
||||
|
||||
.. _retrieving-an-entity:
|
||||
|
||||
Retrieving an entity (user or group)
|
||||
**************************************
|
||||
An “entity” is used to refer to either an `User`__ or a `Chat`__
|
||||
(which includes a `Channel`__). The most straightforward way to get
|
||||
an entity is to use ``TelegramClient.get_entity()``. This method accepts
|
||||
either a string, which can be a username, phone number or `t.me`__-like
|
||||
link, or an integer that will be the ID of an **user**. You can use it
|
||||
like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# all of these work
|
||||
lonami = client.get_entity('lonami')
|
||||
lonami = client.get_entity('t.me/lonami')
|
||||
lonami = client.get_entity('https://telegram.dog/lonami')
|
||||
|
||||
# other kind of entities
|
||||
channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
|
||||
contact = client.get_entity('+34xxxxxxxxx')
|
||||
friend = client.get_entity(friend_id)
|
||||
|
||||
For the last one to work, the library must have “seen” the user at least
|
||||
once. The library will “see” the user as long as any request contains
|
||||
them, so if you’ve called ``.get_dialogs()`` for instance, and your
|
||||
friend was there, the library will know about them. For more, read about
|
||||
the :ref:`sessions`.
|
||||
|
||||
If you want to get a channel or chat by ID, you need to specify that
|
||||
they are a channel or a chat. The library can’t infer what they are by
|
||||
just their ID (unless the ID is marked, but this is only done
|
||||
internally), so you need to wrap the ID around a `Peer`__ object:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
|
||||
my_user = client.get_entity(PeerUser(some_id))
|
||||
my_chat = client.get_entity(PeerChat(some_id))
|
||||
my_channel = client.get_entity(PeerChannel(some_id))
|
||||
|
||||
**Note** that most requests don’t ask for an ``User``, or a ``Chat``,
|
||||
but rather for ``InputUser``, ``InputChat``, and so on. If this is the
|
||||
case, you should prefer ``.get_input_entity()`` over ``.get_entity()``,
|
||||
as it will be immediate if you provide an ID (whereas ``.get_entity()``
|
||||
may need to find who the entity is first).
|
||||
|
||||
Via your open “chats” (dialogs)
|
||||
-------------------------------
|
||||
|
||||
.. note::
|
||||
Please read here: :ref:`retrieving-all-dialogs`.
|
||||
|
||||
Via ResolveUsernameRequest
|
||||
--------------------------
|
||||
|
||||
This is the request used by ``.get_entity`` internally, but you can also
|
||||
use it by hand:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.contacts import ResolveUsernameRequest
|
||||
|
||||
result = client(ResolveUsernameRequest('username'))
|
||||
found_chats = result.chats
|
||||
found_users = result.users
|
||||
# result.peer may be a PeerUser, PeerChat or PeerChannel
|
||||
|
||||
See `Peer`__ for more information about this result.
|
||||
|
||||
Via MessageFwdHeader
|
||||
--------------------
|
||||
|
||||
If all you have is a `MessageFwdHeader`__ after you retrieved a bunch
|
||||
of messages, this gives you access to the ``from_id`` (if forwarded from
|
||||
an user) and ``channel_id`` (if forwarded from a channel). Invoking
|
||||
`GetMessagesRequest`__ also returns a list of ``chats`` and
|
||||
``users``, and you can find the desired entity there:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Logic to retrieve messages with `GetMessagesRequest´
|
||||
messages = foo()
|
||||
fwd_header = bar()
|
||||
|
||||
user = next(u for u in messages.users if u.id == fwd_header.from_id)
|
||||
channel = next(c for c in messages.chats if c.id == fwd_header.channel_id)
|
||||
|
||||
Or you can just call ``.get_entity()`` with the ID, as you should have
|
||||
seen that user or channel before. A call to ``GetMessagesRequest`` may
|
||||
still be neeed.
|
||||
|
||||
Via GetContactsRequest
|
||||
----------------------
|
||||
|
||||
The library will call this for you if you pass a phone number to
|
||||
``.get_entity``, but again, it can be done manually. If the user you
|
||||
want to talk to is a contact, you can use `GetContactsRequest`__:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.contacts import GetContactsRequest
|
||||
from telethon.tl.types.contacts import Contacts
|
||||
|
||||
contacts = client(GetContactsRequest(0))
|
||||
if isinstance(contacts, Contacts):
|
||||
users = contacts.users
|
||||
contacts = contacts.contacts
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/types/user.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/chat.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel.html
|
||||
__ https://t.me
|
||||
__ https://lonamiwebs.github.io/Telethon/types/peer.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/peer.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/message_fwd_header.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/contacts/get_contacts.html
|
||||
|
||||
|
||||
.. _retrieving-all-dialogs:
|
||||
|
||||
Retrieving all dialogs
|
||||
***********************
|
||||
|
||||
There are several ``offset_xyz=`` parameters that have no effect at all,
|
||||
but there's not much one can do since this is something the server should handle.
|
||||
Currently, the only way to get all dialogs
|
||||
(open chats, conversations, etc.) is by using the ``offset_date``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import GetDialogsRequest
|
||||
from telethon.tl.types import InputPeerEmpty
|
||||
from time import sleep
|
||||
|
||||
dialogs = []
|
||||
users = []
|
||||
chats = []
|
||||
|
||||
last_date = None
|
||||
chunk_size = 20
|
||||
while True:
|
||||
result = client(GetDialogsRequest(
|
||||
offset_date=last_date,
|
||||
offset_id=0,
|
||||
offset_peer=InputPeerEmpty(),
|
||||
limit=chunk_size
|
||||
))
|
||||
dialogs.extend(result.dialogs)
|
||||
users.extend(result.users)
|
||||
chats.extend(result.chats)
|
||||
if not result.messages:
|
||||
break
|
||||
last_date = min(msg.date for msg in result.messages)
|
||||
sleep(2)
|
||||
|
||||
|
||||
Joining a chat or channel
|
||||
*******************************
|
||||
|
||||
Note that `Chat`__\ s are normal groups, and `Channel`__\ s are a
|
||||
special form of `Chat`__\ s,
|
||||
which can also be super-groups if their ``megagroup`` member is
|
||||
``True``.
|
||||
|
||||
Joining a public channel
|
||||
------------------------
|
||||
|
||||
Once you have the :ref:`entity <retrieving-an-entity>`
|
||||
of the channel you want to join to, you can
|
||||
make use of the `JoinChannelRequest`__ to join such channel:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.channels import JoinChannelRequest
|
||||
client(JoinChannelRequest(channel))
|
||||
|
||||
# In the same way, you can also leave such channel
|
||||
from telethon.tl.functions.channels import LeaveChannelRequest
|
||||
client(LeaveChannelRequest(input_channel))
|
||||
|
||||
For more on channels, check the `channels namespace`__.
|
||||
|
||||
Joining a private chat or channel
|
||||
---------------------------------
|
||||
|
||||
If all you have is a link like this one:
|
||||
``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have
|
||||
enough information to join! The part after the
|
||||
``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this
|
||||
example, is the ``hash`` of the chat or channel. Now you can use
|
||||
`ImportChatInviteRequest`__ as follows:
|
||||
|
||||
.. -block:: python
|
||||
|
||||
from telethon.tl.functions.messages import ImportChatInviteRequest
|
||||
updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))
|
||||
|
||||
Adding someone else to such chat or channel
|
||||
-------------------------------------------
|
||||
|
||||
If you don’t want to add yourself, maybe because you’re already in, you
|
||||
can always add someone else with the `AddChatUserRequest`__, which
|
||||
use is very straightforward:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import AddChatUserRequest
|
||||
|
||||
client(AddChatUserRequest(
|
||||
chat_id,
|
||||
user_to_add,
|
||||
fwd_limit=10 # allow the user to see the 10 last messages
|
||||
))
|
||||
|
||||
Checking a link without joining
|
||||
-------------------------------
|
||||
|
||||
If you don’t need to join but rather check whether it’s a group or a
|
||||
channel, you can use the `CheckChatInviteRequest`__, which takes in
|
||||
the `hash`__ of said channel or group.
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/chat.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/chat.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html
|
||||
__ https://github.com/LonamiWebs/Telethon/wiki/Joining-a-chat-or-channel#joining-a-private-chat-or-channel
|
||||
|
||||
|
||||
Retrieving all chat members (channels too)
|
||||
******************************************
|
||||
|
||||
In order to get all the members from a mega-group or channel, you need
|
||||
to use `GetParticipantsRequest`__. As we can see it needs an
|
||||
`InputChannel`__, (passing the mega-group or channel you’re going to
|
||||
use will work), and a mandatory `ChannelParticipantsFilter`__. The
|
||||
closest thing to “no filter” is to simply use
|
||||
`ChannelParticipantsSearch`__ with an empty ``'q'`` string.
|
||||
|
||||
If we want to get *all* the members, we need to use a moving offset and
|
||||
a fixed limit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.channels import GetParticipantsRequest
|
||||
from telethon.tl.types import ChannelParticipantsSearch
|
||||
from time import sleep
|
||||
|
||||
offset = 0
|
||||
limit = 100
|
||||
all_participants = []
|
||||
|
||||
while True:
|
||||
participants = client.invoke(GetParticipantsRequest(
|
||||
channel, ChannelParticipantsSearch(''), offset, limit
|
||||
))
|
||||
if not participants.users:
|
||||
break
|
||||
all_participants.extend(participants.users)
|
||||
offset += len(participants.users)
|
||||
# sleep(1) # This line seems to be optional, no guarantees!
|
||||
|
||||
Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__,
|
||||
which may have more information you need (like the role of the
|
||||
participants, total count of members, etc.)
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html
|
||||
|
||||
|
||||
Recent Actions
|
||||
********************
|
||||
|
||||
“Recent actions” is simply the name official applications have given to
|
||||
the “admin log”. Simply use `GetAdminLogRequest`__ for that, and
|
||||
you’ll get AdminLogResults.events in return which in turn has the final
|
||||
`.action`__.
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html
|
||||
|
||||
|
||||
Increasing View Count in a Channel
|
||||
****************************************
|
||||
|
||||
It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
|
||||
while I don’t understand why so many people ask this, the solution is to
|
||||
use `GetMessagesViewsRequest`__, setting ``increment=True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
# Obtain `channel' through dialogs or through client.get_entity() or anyhow.
|
||||
# Obtain `msg_ids' through `.get_message_history()` or anyhow. Must be a list.
|
||||
|
||||
client(GetMessagesViewsRequest(
|
||||
peer=channel,
|
||||
id=msg_ids,
|
||||
increment=True
|
||||
))
|
||||
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/233
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/305
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/409
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/447
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html
|
|
@ -1,103 +0,0 @@
|
|||
=========================
|
||||
Working with messages
|
||||
=========================
|
||||
|
||||
.. note::
|
||||
Make sure you have gone through :ref:`prelude` already!
|
||||
|
||||
|
||||
Forwarding messages
|
||||
*******************
|
||||
|
||||
Note that ForwardMessageRequest_ (note it's Message, singular) will *not* work if channels are involved.
|
||||
This is because channel (and megagroups) IDs are not unique, so you also need to know who the sender is
|
||||
(a parameter this request doesn't have).
|
||||
|
||||
Either way, you are encouraged to use ForwardMessagesRequest_ (note it's Message*s*, plural) *always*,
|
||||
since it is more powerful, as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import ForwardMessagesRequest
|
||||
# note the s ^
|
||||
|
||||
messages = foo() # retrieve a few messages (or even one, in a list)
|
||||
from_entity = bar()
|
||||
to_entity = baz()
|
||||
|
||||
client(ForwardMessagesRequest(
|
||||
from_peer=from_entity, # who sent these messages?
|
||||
id=[msg.id for msg in messages], # which are the messages?
|
||||
to_peer=to_entity # who are we forwarding them to?
|
||||
))
|
||||
|
||||
The named arguments are there for clarity, although they're not needed because they appear in order.
|
||||
You can obviously just wrap a single message on the list too, if that's all you have.
|
||||
|
||||
|
||||
Searching Messages
|
||||
*******************
|
||||
|
||||
Messages are searched through the obvious SearchRequest_, but you may run into issues_. A valid example would be:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
result = client(SearchRequest(
|
||||
entity, 'query', InputMessagesFilterEmpty(), None, None, 0, 0, 100
|
||||
))
|
||||
|
||||
It's important to note that the optional parameter ``from_id`` has been left omitted and thus defaults to ``None``.
|
||||
Changing it to InputUserEmpty_, as one could think to specify "no user", won't work because this parameter is a flag,
|
||||
and it being unspecified has a different meaning.
|
||||
|
||||
If one were to set ``from_id=InputUserEmpty()``, it would filter messages from "empty" senders,
|
||||
which would likely match no users.
|
||||
|
||||
If you get a ``ChatAdminRequiredError`` on a channel, it's probably because you tried setting the ``from_id`` filter,
|
||||
and as the error says, you can't do that. Leave it set to ``None`` and it should work.
|
||||
|
||||
As with every method, make sure you use the right ID/hash combination for your ``InputUser`` or ``InputChat``,
|
||||
or you'll likely run into errors like ``UserIdInvalidError``.
|
||||
|
||||
|
||||
Sending stickers
|
||||
*****************
|
||||
|
||||
Stickers are nothing else than ``files``, and when you successfully retrieve the stickers for a certain sticker set,
|
||||
all you will have are ``handles`` to these files. Remember, the files Telegram holds on their servers can be referenced
|
||||
through this pair of ID/hash (unique per user), and you need to use this handle when sending a "document" message.
|
||||
This working example will send yourself the very first sticker you have:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Get all the sticker sets this user has
|
||||
sticker_sets = client(GetAllStickersRequest(0))
|
||||
|
||||
# Choose a sticker set
|
||||
sticker_set = sticker_sets.sets[0]
|
||||
|
||||
# Get the stickers for this sticker set
|
||||
stickers = client(GetStickerSetRequest(
|
||||
stickerset=InputStickerSetID(
|
||||
id=sticker_set.id, access_hash=sticker_set.access_hash
|
||||
)
|
||||
))
|
||||
|
||||
# Stickers are nothing more than files, so send that
|
||||
client(SendMediaRequest(
|
||||
peer=client.get_me(),
|
||||
media=InputMediaDocument(
|
||||
id=InputDocument(
|
||||
id=stickers.documents[0].id,
|
||||
access_hash=stickers.documents[0].access_hash
|
||||
),
|
||||
caption=''
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
.. _ForwardMessageRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_message.html
|
||||
.. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html
|
||||
.. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html
|
||||
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215
|
||||
.. _InputUserEmpty: https://lonamiwebs.github.io/Telethon/constructors/input_user_empty.html
|
|
@ -1,48 +0,0 @@
|
|||
.. _prelude:
|
||||
|
||||
Prelude
|
||||
---------
|
||||
|
||||
Before reading any specific example, make sure to read the following common steps:
|
||||
|
||||
All the examples assume that you have successfully created a client and you're authorized as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient
|
||||
|
||||
# Use your own values here
|
||||
api_id = 12345
|
||||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
phone_number = '+34600000000'
|
||||
|
||||
client = TelegramClient('some_name', api_id, api_hash)
|
||||
client.connect() # Must return True, otherwise, try again
|
||||
|
||||
if not client.is_user_authorized():
|
||||
client.send_code_request(phone_number)
|
||||
# .sign_in() may raise PhoneNumberUnoccupiedError
|
||||
# In that case, you need to call .sign_up() to get a new account
|
||||
client.sign_in(phone_number, input('Enter code: '))
|
||||
|
||||
# The `client´ is now ready
|
||||
|
||||
Although Python will probably clean up the resources used by the ``TelegramClient``,
|
||||
you should always ``.disconnect()`` it once you're done:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
try:
|
||||
# Code using the client goes here
|
||||
except:
|
||||
# No matter what happens, always disconnect in the end
|
||||
client.disconnect()
|
||||
|
||||
If the examples aren't enough, you're strongly advised to read the source code
|
||||
for the InteractiveTelegramClient_ for an overview on how you could build your next script.
|
||||
This example shows a basic usage more than enough in most cases. Even reading the source
|
||||
for the TelegramClient_ may help a lot!
|
||||
|
||||
|
||||
.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py
|
||||
.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py
|
|
@ -1,117 +0,0 @@
|
|||
.. _accessing-the-full-api:
|
||||
|
||||
==========================
|
||||
Accessing the Full API
|
||||
==========================
|
||||
|
||||
The ``TelegramClient`` doesn’t offer a method for every single request
|
||||
the Telegram API supports. However, it’s very simple to ``.invoke()``
|
||||
any request. Whenever you need something, don’t forget to `check the
|
||||
documentation`__ and look for the `method you need`__. There you can go
|
||||
through a sorted list of everything you can do.
|
||||
|
||||
You should also refer to the documentation to see what the objects
|
||||
(constructors) Telegram returns look like. Every constructor inherits
|
||||
from a common type, and that’s the reason for this distinction.
|
||||
|
||||
Say ``client.send_message()`` didn’t exist, we could use the `search`__
|
||||
to look for “message”. There we would find `SendMessageRequest`__,
|
||||
which we can work with.
|
||||
|
||||
Every request is a Python class, and has the parameters needed for you
|
||||
to invoke it. You can also call ``help(request)`` for information on
|
||||
what input parameters it takes. Remember to “Copy import to the
|
||||
clipboard”, or your script won’t be aware of this class! Now we have:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import SendMessageRequest
|
||||
|
||||
If you’re going to use a lot of these, you may do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import telethon.tl.functions as tl
|
||||
# We now have access to 'tl.messages.SendMessageRequest'
|
||||
|
||||
We see that this request must take at least two parameters, a ``peer``
|
||||
of type `InputPeer`__, and a ``message`` which is just a Python
|
||||
``str``\ ing.
|
||||
|
||||
How can we retrieve this ``InputPeer``? We have two options. We manually
|
||||
`construct one`__, for instance:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.types import InputPeerUser
|
||||
|
||||
peer = InputPeerUser(user_id, user_hash)
|
||||
|
||||
Or we call ``.get_input_entity()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
peer = client.get_input_entity('someone')
|
||||
|
||||
When you’re going to invoke an API method, most require you to pass an
|
||||
``InputUser``, ``InputChat``, or so on, this is why using
|
||||
``.get_input_entity()`` is more straightforward (and sometimes
|
||||
immediate, if you know the ID of the user for instance). If you also
|
||||
need to have information about the whole user, use ``.get_entity()``
|
||||
instead:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
entity = client.get_entity('someone')
|
||||
|
||||
In the later case, when you use the entity, the library will cast it to
|
||||
its “input” version for you. If you already have the complete user and
|
||||
want to cache its input version so the library doesn’t have to do this
|
||||
every time its used, simply call ``.get_input_peer``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import utils
|
||||
peer = utils.get_input_user(entity)
|
||||
|
||||
After this small parenthesis about ``.get_entity`` versus
|
||||
``.get_input_entity``, we have everything we need. To ``.invoke()`` our
|
||||
request we do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
result = client(SendMessageRequest(peer, 'Hello there!'))
|
||||
# __call__ is an alias for client.invoke(request). Both will work
|
||||
|
||||
Message sent! Of course, this is only an example.
|
||||
There are nearly 250 methods available as of layer 73,
|
||||
and you can use every single of them as you wish.
|
||||
Remember to use the right types! To sum up:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
result = client(SendMessageRequest(
|
||||
client.get_input_entity('username'), 'Hello there!'
|
||||
))
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Note that some requests have a "hash" parameter. This is **not** your ``api_hash``!
|
||||
It likely isn't your self-user ``.access_hash`` either.
|
||||
It's a special hash used by Telegram to only send a difference of new data
|
||||
that you don't already have with that request,
|
||||
so you can leave it to 0, and it should work (which means no hash is known yet).
|
||||
|
||||
For those requests having a "limit" parameter,
|
||||
you can often set it to zero to signify "return as many items as possible".
|
||||
This won't work for all of them though,
|
||||
for instance, in "messages.search" it will actually return 0 items.
|
||||
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/index.html
|
||||
__ https://lonamiwebs.github.io/Telethon/?q=message
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/send_message.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/input_peer.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/input_peer_user.html
|
|
@ -1,24 +1,28 @@
|
|||
.. _creating-a-client:
|
||||
|
||||
===================
|
||||
=================
|
||||
Creating a Client
|
||||
===================
|
||||
=================
|
||||
|
||||
|
||||
Before working with Telegram's API, you need to get your own API ID and hash:
|
||||
|
||||
1. Follow `this link <https://my.telegram.org/>`_ and login with your phone number.
|
||||
1. Follow `this link <https://my.telegram.org/>`_ and login with your
|
||||
phone number.
|
||||
|
||||
2. Click under API Development tools.
|
||||
|
||||
3. A *Create new application* window will appear. Fill in your application details.
|
||||
There is no need to enter any *URL*, and only the first two fields (*App title* and *Short name*)
|
||||
can be changed later as far as I'm aware.
|
||||
3. A *Create new application* window will appear. Fill in your application
|
||||
details. There is no need to enter any *URL*, and only the first two
|
||||
fields (*App title* and *Short name*) can currently be changed later.
|
||||
|
||||
4. Click on *Create application* at the end. Remember that your **API hash is secret**
|
||||
and Telegram won't let you revoke it. Don't post it anywhere!
|
||||
4. Click on *Create application* at the end. Remember that your
|
||||
**API hash is secret** and Telegram won't let you revoke it.
|
||||
Don't post it anywhere!
|
||||
|
||||
Once that's ready, the next step is to create a ``TelegramClient``.
|
||||
This class will be your main interface with Telegram's API, and creating one is very simple:
|
||||
This class will be your main interface with Telegram's API, and creating
|
||||
one is very simple:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -27,18 +31,21 @@ This class will be your main interface with Telegram's API, and creating one is
|
|||
# Use your own values here
|
||||
api_id = 12345
|
||||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
phone_number = '+34600000000'
|
||||
|
||||
client = TelegramClient('some_name', api_id, api_hash)
|
||||
|
||||
Note that ``'some_name'`` will be used to save your session (persistent information such as access key and others)
|
||||
as ``'some_name.session'`` in your disk. This is simply a JSON file which you can (but shouldn't) modify.
|
||||
|
||||
Before using the client, you must be connected to Telegram. Doing so is very easy:
|
||||
Note that ``'some_name'`` will be used to save your session (persistent
|
||||
information such as access key and others) as ``'some_name.session'`` in
|
||||
your disk. This is by default a database file using Python's ``sqlite3``.
|
||||
|
||||
Before using the client, you must be connected to Telegram.
|
||||
Doing so is very easy:
|
||||
|
||||
``client.connect() # Must return True, otherwise, try again``
|
||||
|
||||
You may or may not be authorized yet. You must be authorized before you're able to send any request:
|
||||
You may or may not be authorized yet. You must be authorized
|
||||
before you're able to send any request:
|
||||
|
||||
``client.is_user_authorized() # Returns True if you can send requests``
|
||||
|
||||
|
@ -46,19 +53,65 @@ If you're not authorized, you need to ``.sign_in()``:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
phone_number = '+34600000000'
|
||||
client.send_code_request(phone_number)
|
||||
myself = client.sign_in(phone_number, input('Enter code: '))
|
||||
# If .sign_in raises PhoneNumberUnoccupiedError, use .sign_up instead
|
||||
# If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...)
|
||||
# You can import both exceptions from telethon.errors.
|
||||
|
||||
``myself`` is your Telegram user.
|
||||
You can view all the information about yourself by doing ``print(myself.stringify())``.
|
||||
You're now ready to use the client as you wish!
|
||||
.. note::
|
||||
|
||||
If you send the code that Telegram sent you over the app through the
|
||||
app itself, it will expire immediately. You can still send the code
|
||||
through the app by "obfuscating" it (maybe add a magic constant, like
|
||||
``12345``, and then subtract it to get the real code back) or any other
|
||||
technique.
|
||||
|
||||
``myself`` is your Telegram user. You can view all the information about
|
||||
yourself by doing ``print(myself.stringify())``. You're now ready to use
|
||||
the client as you wish! Remember that any object returned by the API has
|
||||
mentioned ``.stringify()`` method, and printing these might prove useful.
|
||||
|
||||
As a full example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient('anon', api_id, api_hash)
|
||||
assert client.connect()
|
||||
if not client.is_user_authorized():
|
||||
client.send_code_request(phone_number)
|
||||
me = client.sign_in(phone_number, input('Enter code: '))
|
||||
|
||||
|
||||
All of this, however, can be done through a call to ``.start()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient('anon', api_id, api_hash)
|
||||
client.start()
|
||||
|
||||
|
||||
The code shown is just what ``.start()`` will be doing behind the scenes
|
||||
(with a few extra checks), so that you know how to sign in case you want
|
||||
to avoid using ``input()`` (the default) for whatever reason. If no phone
|
||||
or bot token is provided, you will be asked one through ``input()``. The
|
||||
method also accepts a ``phone=`` and ``bot_token`` parameters.
|
||||
|
||||
You can use either, as both will work. Determining which
|
||||
is just a matter of taste, and how much control you need.
|
||||
|
||||
Remember that you can get yourself at any time with ``client.get_me()``.
|
||||
|
||||
.. warning::
|
||||
Please note that if you fail to login around 5 times (or change the first
|
||||
parameter of the ``TelegramClient``, which is the session name) you will
|
||||
receive a ``FloodWaitError`` of around 22 hours, so be careful not to mess
|
||||
this up! This shouldn't happen if you're doing things as explained, though.
|
||||
|
||||
.. note::
|
||||
If you want to use a **proxy**, you have to `install PySocks`__ (via pip or manual)
|
||||
and then set the appropriated parameters:
|
||||
If you want to use a **proxy**, you have to `install PySocks`__
|
||||
(via pip or manual) and then set the appropriated parameters:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -72,5 +125,69 @@ You're now ready to use the client as you wish!
|
|||
consisting of parameters described `here`__.
|
||||
|
||||
|
||||
|
||||
Two Factor Authorization (2FA)
|
||||
******************************
|
||||
|
||||
If you have Two Factor Authorization (from now on, 2FA) enabled on your
|
||||
account, calling :meth:`telethon.TelegramClient.sign_in` will raise a
|
||||
``SessionPasswordNeededError``. When this happens, just
|
||||
:meth:`telethon.TelegramClient.sign_in` again with a ``password=``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import getpass
|
||||
from telethon.errors import SessionPasswordNeededError
|
||||
|
||||
client.sign_in(phone)
|
||||
try:
|
||||
client.sign_in(code=input('Enter code: '))
|
||||
except SessionPasswordNeededError:
|
||||
client.sign_in(password=getpass.getpass())
|
||||
|
||||
|
||||
The mentioned ``.start()`` method will handle this for you as well, but
|
||||
you must set the ``password=`` parameter beforehand (it won't be asked).
|
||||
|
||||
If you don't have 2FA enabled, but you would like to do so through the library,
|
||||
use ``client.edit_2fa()``.
|
||||
Be sure to know what you're doing when using this function and
|
||||
you won't run into any problems.
|
||||
Take note that if you want to set only the email/hint and leave
|
||||
the current password unchanged, you need to "redo" the 2fa.
|
||||
|
||||
See the examples below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.errors import EmailUnconfirmedError
|
||||
|
||||
# Sets 2FA password for first time:
|
||||
client.edit_2fa(new_password='supersecurepassword')
|
||||
|
||||
# Changes password:
|
||||
client.edit_2fa(current_password='supersecurepassword',
|
||||
new_password='changedmymind')
|
||||
|
||||
# Clears current password (i.e. removes 2FA):
|
||||
client.edit_2fa(current_password='changedmymind', new_password=None)
|
||||
|
||||
# Sets new password with recovery email:
|
||||
try:
|
||||
client.edit_2fa(new_password='memes and dreams',
|
||||
email='JohnSmith@example.com')
|
||||
# Raises error (you need to check your email to complete 2FA setup.)
|
||||
except EmailUnconfirmedError:
|
||||
# You can put email checking code here if desired.
|
||||
pass
|
||||
|
||||
# Also take note that unless you remove 2FA or explicitly
|
||||
# give email parameter again it will keep the last used setting
|
||||
|
||||
# Set hint after already setting password:
|
||||
client.edit_2fa(current_password='memes and dreams',
|
||||
new_password='memes and dreams',
|
||||
hint='It keeps you alive')
|
||||
|
||||
__ https://github.com/Anorov/PySocks#installation
|
||||
__ https://github.com/Anorov/PySocks#usage-1%3E
|
||||
__ https://github.com/Anorov/PySocks#usage-1
|
||||
|
|
120
readthedocs/extra/basic/entities.rst
Normal file
120
readthedocs/extra/basic/entities.rst
Normal file
|
@ -0,0 +1,120 @@
|
|||
.. _entities:
|
||||
|
||||
=========================
|
||||
Users, Chats and Channels
|
||||
=========================
|
||||
|
||||
|
||||
Introduction
|
||||
************
|
||||
|
||||
The library widely uses the concept of "entities". An entity will refer
|
||||
to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return
|
||||
in response to certain methods, such as :tl:`GetUsersRequest`.
|
||||
|
||||
.. note::
|
||||
|
||||
When something "entity-like" is required, it means that you need to
|
||||
provide something that can be turned into an entity. These things include,
|
||||
but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects,
|
||||
or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even
|
||||
phone numbers from people you have in your contacts.
|
||||
|
||||
Getting entities
|
||||
****************
|
||||
|
||||
Through the use of the :ref:`sessions`, the library will automatically
|
||||
remember the ID and hash pair, along with some extra information, so
|
||||
you're able to just do this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Dialogs are the "conversations you have open".
|
||||
# This method returns a list of Dialog, which
|
||||
# has the .entity attribute and other information.
|
||||
dialogs = client.get_dialogs()
|
||||
|
||||
# All of these work and do the same.
|
||||
lonami = client.get_entity('lonami')
|
||||
lonami = client.get_entity('t.me/lonami')
|
||||
lonami = client.get_entity('https://telegram.dog/lonami')
|
||||
|
||||
# Other kind of entities.
|
||||
channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
|
||||
contact = client.get_entity('+34xxxxxxxxx')
|
||||
friend = client.get_entity(friend_id)
|
||||
|
||||
# Getting entities through their ID (User, Chat or Channel)
|
||||
entity = client.get_entity(some_id)
|
||||
|
||||
# You can be more explicit about the type for said ID by wrapping
|
||||
# it inside a Peer instance. This is recommended but not necessary.
|
||||
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
|
||||
|
||||
my_user = client.get_entity(PeerUser(some_id))
|
||||
my_chat = client.get_entity(PeerChat(some_id))
|
||||
my_channel = client.get_entity(PeerChannel(some_id))
|
||||
|
||||
|
||||
All methods in the :ref:`telegram-client` call ``.get_input_entity()`` prior
|
||||
to sending the requst to save you from the hassle of doing so manually.
|
||||
That way, convenience calls such as ``client.send_message('lonami', 'hi!')``
|
||||
become possible.
|
||||
|
||||
Every entity the library encounters (in any response to any call) will by
|
||||
default be cached in the ``.session`` file (an SQLite database), to avoid
|
||||
performing unnecessary API calls. If the entity cannot be found, additonal
|
||||
calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be
|
||||
made to obtain the required information.
|
||||
|
||||
|
||||
Entities vs. Input Entities
|
||||
***************************
|
||||
|
||||
.. note::
|
||||
|
||||
Don't worry if you don't understand this section, just remember some
|
||||
of the details listed here are important. When you're calling a method,
|
||||
don't call ``.get_entity()`` beforehand, just use the username or phone,
|
||||
or the entity retrieved by other means like ``.get_dialogs()``.
|
||||
|
||||
|
||||
On top of the normal types, the API also make use of what they call their
|
||||
``Input*`` versions of objects. The input version of an entity (e.g.
|
||||
:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum
|
||||
information that's required from Telegram to be able to identify
|
||||
who you're referring to: a :tl:`Peer`'s **ID** and **hash**.
|
||||
|
||||
This ID/hash pair is unique per user, so if you use the pair given by another
|
||||
user **or bot** it will **not** work.
|
||||
|
||||
To save *even more* bandwidth, the API also makes use of the :tl:`Peer`
|
||||
versions, which just have an ID. This serves to identify them, but
|
||||
peers alone are not enough to use them. You need to know their hash
|
||||
before you can "use them".
|
||||
|
||||
As we just mentioned, API calls don't need to know the whole information
|
||||
about the entities, only their ID and hash. For this reason, another method,
|
||||
``.get_input_entity()`` is available. This will always use the cache while
|
||||
possible, making zero API calls most of the time. When a request is made,
|
||||
if you provided the full entity, e.g. an :tl:`User`, the library will convert
|
||||
it to the required :tl:`InputPeer` automatically for you.
|
||||
|
||||
**You should always favour** ``.get_input_entity()`` **over** ``.get_entity()``
|
||||
for this reason! Calling the latter will always make an API call to get
|
||||
the most recent information about said entity, but invoking requests don't
|
||||
need this information, just the ``InputPeer``. Only use ``.get_entity()``
|
||||
if you need to get actual information, like the username, name, title, etc.
|
||||
of the entity.
|
||||
|
||||
To further simplify the workflow, since the version ``0.16.2`` of the
|
||||
library, the raw requests you make to the API are also able to call
|
||||
``.get_input_entity`` wherever needed, so you can even do things like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client(SendMessageRequest('username', 'hello'))
|
||||
|
||||
The library will call the ``.resolve()`` method of the request, which will
|
||||
resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if
|
||||
you don't get this yet, but remember some of the details here are important.
|
|
@ -1,23 +1,21 @@
|
|||
.. Telethon documentation master file, created by
|
||||
sphinx-quickstart on Fri Nov 17 15:36:11 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
.. _getting-started:
|
||||
|
||||
|
||||
=================
|
||||
Getting Started!
|
||||
=================
|
||||
===============
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
|
||||
Simple Installation
|
||||
*********************
|
||||
*******************
|
||||
|
||||
``pip install telethon``
|
||||
``pip3 install telethon``
|
||||
|
||||
**More details**: :ref:`installation`
|
||||
|
||||
|
||||
Creating a client
|
||||
**************
|
||||
*****************
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -27,28 +25,68 @@ Creating a client
|
|||
# api_hash from https://my.telegram.org, under API Development.
|
||||
api_id = 12345
|
||||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
phone = '+34600000000'
|
||||
|
||||
client = TelegramClient('session_name', api_id, api_hash)
|
||||
client.connect()
|
||||
|
||||
# If you already have a previous 'session_name.session' file, skip this.
|
||||
client.sign_in(phone=phone)
|
||||
me = client.sign_in(code=77777) # Put whatever code you received here.
|
||||
client.start()
|
||||
|
||||
**More details**: :ref:`creating-a-client`
|
||||
|
||||
|
||||
Simple Stuff
|
||||
**************
|
||||
Basic Usage
|
||||
***********
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(me.stringify())
|
||||
# Getting information about yourself
|
||||
print(client.get_me().stringify())
|
||||
|
||||
client.send_message('username', 'Hello! Talking to you from Telethon')
|
||||
# Sending a message (you can use 'me' or 'self' to message yourself)
|
||||
client.send_message('username', 'Hello World from Telethon!')
|
||||
|
||||
# Sending a file
|
||||
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
|
||||
|
||||
client.download_profile_photo(me)
|
||||
total, messages, senders = client.get_message_history('username')
|
||||
# Retrieving messages from a chat
|
||||
from telethon import utils
|
||||
for message in client.get_message_history('username', limit=10):
|
||||
print(utils.get_display_name(message.sender), message.message)
|
||||
|
||||
# Listing all the dialogs (conversations you have open)
|
||||
for dialog in client.get_dialogs(limit=10):
|
||||
print(utils.get_display_name(dialog.entity), dialog.draft.message)
|
||||
|
||||
# Downloading profile photos (default path is the working directory)
|
||||
client.download_profile_photo('username')
|
||||
|
||||
# Once you have a message with .media (if message.media)
|
||||
# you can download it using client.download_media():
|
||||
messages = client.get_message_history('username')
|
||||
client.download_media(messages[0])
|
||||
|
||||
**More details**: :ref:`telegram-client`
|
||||
|
||||
|
||||
Handling Updates
|
||||
****************
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
# We need to have some worker running
|
||||
client.updates.workers = 1
|
||||
|
||||
@client.on(events.NewMessage(incoming=True, pattern='(?i)hi'))
|
||||
def handler(event):
|
||||
event.reply('Hello!')
|
||||
|
||||
# If you want to handle updates you can't let the script end.
|
||||
input('Press enter to exit.')
|
||||
|
||||
**More details**: :ref:`working-with-updates`
|
||||
|
||||
|
||||
----------
|
||||
|
||||
You can continue by clicking on the "More details" link below each
|
||||
snippet of code or the "Next" button at the bottom of the page.
|
||||
|
|
|
@ -1,44 +1,56 @@
|
|||
.. _installation:
|
||||
|
||||
=================
|
||||
============
|
||||
Installation
|
||||
=================
|
||||
============
|
||||
|
||||
|
||||
Automatic Installation
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
**********************
|
||||
|
||||
To install Telethon, simply do:
|
||||
|
||||
``pip install telethon``
|
||||
``pip3 install telethon``
|
||||
|
||||
If you get something like ``"SyntaxError: invalid syntax"`` or any other error while installing,
|
||||
it's probably because ``pip`` defaults to Python 2, which is not supported. Use ``pip3`` instead.
|
||||
Needless to say, you must have Python 3 and PyPi installed in your system.
|
||||
See https://python.org and https://pypi.python.org/pypi/pip for more.
|
||||
|
||||
If you already have the library installed, upgrade with:
|
||||
|
||||
``pip install --upgrade telethon``
|
||||
``pip3 install --upgrade telethon``
|
||||
|
||||
You can also install the library directly from GitHub or a fork:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: sh
|
||||
|
||||
# pip install git+https://github.com/LonamiWebs/Telethon.git
|
||||
# pip3 install git+https://github.com/LonamiWebs/Telethon.git
|
||||
or
|
||||
$ git clone https://github.com/LonamiWebs/Telethon.git
|
||||
$ cd Telethon/
|
||||
# pip install -Ue .
|
||||
|
||||
If you don't have root access, simply pass the ``--user`` flag to the pip command.
|
||||
If you don't have root access, simply pass the ``--user`` flag to the pip
|
||||
command. If you want to install a specific branch, append ``@branch`` to
|
||||
the end of the first install command.
|
||||
|
||||
By default the library will use a pure Python implementation for encryption,
|
||||
which can be really slow when uploading or downloading files. If you don't
|
||||
mind using a C extension, install `cryptg <https://github.com/Lonami/cryptg>`__
|
||||
via ``pip`` or as an extra:
|
||||
|
||||
``pip3 install telethon[cryptg]``
|
||||
|
||||
|
||||
Manual Installation
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
*******************
|
||||
|
||||
1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and ``rsa`` (`GitHub`__ | `PyPi`__) modules:
|
||||
1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and
|
||||
``rsa`` (`GitHub`__ | `PyPi`__) modules:
|
||||
|
||||
``sudo -H pip install pyaes rsa``
|
||||
``sudo -H pip3 install pyaes rsa``
|
||||
|
||||
2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git``
|
||||
2. Clone Telethon's GitHub repository:
|
||||
``git clone https://github.com/LonamiWebs/Telethon.git``
|
||||
|
||||
3. Enter the cloned repository: ``cd Telethon``
|
||||
|
||||
|
@ -46,26 +58,22 @@ Manual Installation
|
|||
|
||||
5. Done!
|
||||
|
||||
To generate the documentation, ``cd docs`` and then ``python3 generate.py``.
|
||||
To generate the `method documentation`__, ``cd docs`` and then
|
||||
``python3 generate.py`` (if some pages render bad do it twice).
|
||||
|
||||
|
||||
Optional dependencies
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you're using the library under ARM (or even if you aren't),
|
||||
you may want to install ``sympy`` through ``pip`` for a substantial speed-up
|
||||
when generating the keys required to connect to Telegram
|
||||
(you can of course do this on desktop too). See `issue #199`__ for more.
|
||||
|
||||
If ``libssl`` is available on your system, it will also be used wherever encryption is needed.
|
||||
|
||||
If neither of these are available, a pure Python callback will be used instead,
|
||||
so you can still run the library wherever Python is available!
|
||||
*********************
|
||||
|
||||
If the `cryptg`__ is installed, you might notice a speed-up in the download
|
||||
and upload speed, since these are the most cryptographic-heavy part of the
|
||||
library and said module is a C extension. Otherwise, the ``pyaes`` fallback
|
||||
will be used.
|
||||
|
||||
|
||||
__ https://github.com/ricmoo/pyaes
|
||||
__ https://pypi.python.org/pypi/pyaes
|
||||
__ https://github.com/sybrenstuvel/python-rsa/
|
||||
__ https://github.com/sybrenstuvel/python-rsa
|
||||
__ https://pypi.python.org/pypi/rsa/3.4.2
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/199
|
||||
__ https://lonamiwebs.github.io/Telethon
|
||||
__ https://github.com/Lonami/cryptg
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
.. _sending-requests:
|
||||
|
||||
==================
|
||||
Sending Requests
|
||||
==================
|
||||
|
||||
Since we're working with Python, one must not forget that they can do ``help(client)`` or ``help(TelegramClient)``
|
||||
at any time for a more detailed description and a list of all the available methods.
|
||||
Calling ``help()`` from an interactive Python session will always list all the methods for any object, even yours!
|
||||
|
||||
Interacting with the Telegram API is done through sending **requests**,
|
||||
this is, any "method" listed on the API. There are a few methods on the ``TelegramClient`` class
|
||||
that abstract you from the need of manually importing the requests you need.
|
||||
|
||||
For instance, retrieving your own user can be done in a single line:
|
||||
|
||||
``myself = client.get_me()``
|
||||
|
||||
Internally, this method has sent a request to Telegram, who replied with the information about your own user.
|
||||
|
||||
If you want to retrieve any other user, chat or channel (channels are a special subset of chats),
|
||||
you want to retrieve their "entity". This is how the library refers to either of these:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# The method will infer that you've passed an username
|
||||
# It also accepts phone numbers, and will get the user
|
||||
# from your contact list.
|
||||
lonami = client.get_entity('lonami')
|
||||
|
||||
Note that saving and using these entities will be more important when Accessing the Full API.
|
||||
For now, this is a good way to get information about an user or chat.
|
||||
|
||||
Other common methods for quick scripts are also available:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Sending a message (use an entity/username/etc)
|
||||
client.send_message('TheAyyBot', 'ayy')
|
||||
|
||||
# Sending a photo, or a file
|
||||
client.send_file(myself, '/path/to/the/file.jpg', force_document=True)
|
||||
|
||||
# Downloading someone's profile photo. File is saved to 'where'
|
||||
where = client.download_profile_photo(someone)
|
||||
|
||||
# Retrieving the message history
|
||||
total, messages, senders = client.get_message_history(someone)
|
||||
|
||||
# Downloading the media from a specific message
|
||||
# You can specify either a directory, a filename, or nothing at all
|
||||
where = client.download_media(message, '/path/to/output')
|
||||
|
||||
Remember that you can call ``.stringify()`` to any object Telegram returns to pretty print it.
|
||||
Calling ``str(result)`` does the same operation, but on a single line.
|
|
@ -1,48 +0,0 @@
|
|||
.. _sessions:
|
||||
|
||||
==============
|
||||
Session Files
|
||||
==============
|
||||
|
||||
The first parameter you pass the constructor of the
|
||||
``TelegramClient`` is the ``session``, and defaults to be the session
|
||||
name (or full path). That is, if you create a ``TelegramClient('anon')``
|
||||
instance and connect, an ``anon.session`` file will be created on the
|
||||
working directory.
|
||||
|
||||
These JSON session files contain the required information to talk to the
|
||||
Telegram servers, such as to which IP the client should connect, port,
|
||||
authorization key so that messages can be encrypted, and so on.
|
||||
|
||||
These files will by default also save all the input entities that you’ve
|
||||
seen, so that you can get information about an user or channel by just
|
||||
their ID. Telegram will **not** send their ``access_hash`` required to
|
||||
retrieve more information about them, if it thinks you have already seem
|
||||
them. For this reason, the library needs to store this information
|
||||
offline.
|
||||
|
||||
The library will by default too save all the entities (users with their
|
||||
name, username, chats and so on) **in memory**, not to disk, so that you
|
||||
can quickly access them by username or phone number. This can be
|
||||
disabled too. Run ``help(client.session.entities)`` to see the available
|
||||
methods (or ``help(EntityDatabase)``).
|
||||
|
||||
If you’re not going to work without updates, or don’t need to cache the
|
||||
``access_hash`` associated with the entities’ ID, you can disable this
|
||||
by setting ``client.session.save_entities = False``.
|
||||
|
||||
If you don’t want to save the files as JSON, you can also create your
|
||||
custom ``Session`` subclass and override the ``.save()`` and ``.load()``
|
||||
methods. For example, you could save it on a database:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DatabaseSession(Session):
|
||||
def save():
|
||||
# serialize relevant data to the database
|
||||
|
||||
def load():
|
||||
# load relevant data to the database
|
||||
|
||||
You should read the ``session.py`` source file to know what “relevant
|
||||
data” you need to keep track of.
|
97
readthedocs/extra/basic/telegram-client.rst
Normal file
97
readthedocs/extra/basic/telegram-client.rst
Normal file
|
@ -0,0 +1,97 @@
|
|||
.. _telegram-client:
|
||||
|
||||
==============
|
||||
TelegramClient
|
||||
==============
|
||||
|
||||
|
||||
Introduction
|
||||
************
|
||||
|
||||
.. note::
|
||||
|
||||
Check the :ref:`telethon-package` if you're looking for the methods
|
||||
reference instead of this tutorial.
|
||||
|
||||
The ``TelegramClient`` is the central class of the library, the one
|
||||
you will be using most of the time. For this reason, it's important
|
||||
to know what it offers.
|
||||
|
||||
Since we're working with Python, one must not forget that we can do
|
||||
``help(client)`` or ``help(TelegramClient)`` at any time for a more
|
||||
detailed description and a list of all the available methods. Calling
|
||||
``help()`` from an interactive Python session will always list all the
|
||||
methods for any object, even yours!
|
||||
|
||||
Interacting with the Telegram API is done through sending **requests**,
|
||||
this is, any "method" listed on the API. There are a few methods (and
|
||||
growing!) on the ``TelegramClient`` class that abstract you from the
|
||||
need of manually importing the requests you need.
|
||||
|
||||
For instance, retrieving your own user can be done in a single line:
|
||||
|
||||
``myself = client.get_me()``
|
||||
|
||||
Internally, this method has sent a request to Telegram, who replied with
|
||||
the information about your own user, and then the desired information
|
||||
was extracted from their response.
|
||||
|
||||
If you want to retrieve any other user, chat or channel (channels are a
|
||||
special subset of chats), you want to retrieve their "entity". This is
|
||||
how the library refers to either of these:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# The method will infer that you've passed an username
|
||||
# It also accepts phone numbers, and will get the user
|
||||
# from your contact list.
|
||||
lonami = client.get_entity('lonami')
|
||||
|
||||
The so called "entities" are another important whole concept on its own,
|
||||
but for now you don't need to worry about it. Simply know that they are
|
||||
a good way to get information about an user, chat or channel.
|
||||
|
||||
Many other common methods for quick scripts are also available:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Note that you can use 'me' or 'self' to message yourself
|
||||
client.send_message('username', 'Hello World from Telethon!')
|
||||
|
||||
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
|
||||
|
||||
# The utils package has some goodies, like .get_display_name()
|
||||
from telethon import utils
|
||||
for message in client.get_message_history('username', limit=10):
|
||||
print(utils.get_display_name(message.sender), message.message)
|
||||
|
||||
# Dialogs are the conversations you have open
|
||||
for dialog in client.get_dialogs(limit=10):
|
||||
print(utils.get_display_name(dialog.entity), dialog.draft.message)
|
||||
|
||||
# Default path is the working directory
|
||||
client.download_profile_photo('username')
|
||||
|
||||
# Call .disconnect() when you're done
|
||||
client.disconnect()
|
||||
|
||||
Remember that you can call ``.stringify()`` to any object Telegram returns
|
||||
to pretty print it. Calling ``str(result)`` does the same operation, but on
|
||||
a single line.
|
||||
|
||||
|
||||
Available methods
|
||||
*****************
|
||||
|
||||
This page lists all the "handy" methods available for you to use in the
|
||||
``TelegramClient`` class. These are simply wrappers around the "raw"
|
||||
Telegram API, making it much more manageable and easier to work with.
|
||||
|
||||
Please refer to :ref:`accessing-the-full-api` if these aren't enough,
|
||||
and don't be afraid to read the source code of the InteractiveTelegramClient_
|
||||
or even the TelegramClient_ itself to learn how it works.
|
||||
|
||||
To see the methods available in the client, see :ref:`telethon-package`.
|
||||
|
||||
.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py
|
||||
.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py
|
|
@ -4,132 +4,185 @@
|
|||
Working with Updates
|
||||
====================
|
||||
|
||||
|
||||
The library comes with the :mod:`events` module. *Events* are an abstraction
|
||||
over what Telegram calls `updates`__, and are meant to ease simple and common
|
||||
usage when dealing with them, since there are many updates. If you're looking
|
||||
for the method reference, check :ref:`telethon-events-package`, otherwise,
|
||||
let's dive in!
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The library logs by default no output, and any exception that occurs
|
||||
inside your handlers will be "hidden" from you to prevent the thread
|
||||
from terminating (so it can still deliver events). You should enable
|
||||
logging (``import logging; logging.basicConfig(level=logging.ERROR)``)
|
||||
when working with events, at least the error level, to see if this is
|
||||
happening so you can debug the error.
|
||||
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
The library can run in four distinguishable modes:
|
||||
|
||||
- With no extra threads at all.
|
||||
- With an extra thread that receives everything as soon as possible (default).
|
||||
- With several worker threads that run your update handlers.
|
||||
- A mix of the above.
|
||||
|
||||
Since this section is about updates, we'll describe the simplest way to work with them.
|
||||
|
||||
.. warning::
|
||||
Remember that you should always call ``client.disconnect()`` once you're done.
|
||||
|
||||
|
||||
Using multiple workers
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When you create your client, simply pass a number to the ``update_workers`` parameter:
|
||||
|
||||
``client = TelegramClient('session', api_id, api_hash, update_workers=4)``
|
||||
|
||||
4 workers should suffice for most cases (this is also the default on `Python Telegram Bot`__).
|
||||
You can set this value to more, or even less if you need.
|
||||
|
||||
The next thing you want to do is to add a method that will be called when an `Update`__ arrives:
|
||||
Getting Started
|
||||
***************
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def callback(update):
|
||||
print('I received', update)
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
client.add_update_handler(callback)
|
||||
# do more work here, or simply sleep!
|
||||
client = TelegramClient(..., update_workers=1, spawn_read_thread=False)
|
||||
client.start()
|
||||
|
||||
That's it! Now let's do something more interesting.
|
||||
Every time an user talks to use, let's reply to them with the same text reversed:
|
||||
@client.on(events.NewMessage)
|
||||
def my_event_handler(event):
|
||||
if 'hello' in event.raw_text:
|
||||
event.reply('hi!')
|
||||
|
||||
client.idle()
|
||||
|
||||
|
||||
Not much, but there might be some things unclear. What does this code do?
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.types import UpdateShortMessage, PeerUser
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
def replier(update):
|
||||
if isinstance(update, UpdateShortMessage) and not update.out:
|
||||
client.send_message(PeerUser(update.user_id), update.message[::-1])
|
||||
client = TelegramClient(..., update_workers=1, spawn_read_thread=False)
|
||||
client.start()
|
||||
|
||||
|
||||
client.add_update_handler(replier)
|
||||
input('Press enter to stop this!')
|
||||
client.disconnect()
|
||||
|
||||
We only ask you one thing: don't keep this running for too long, or your contacts will go mad.
|
||||
|
||||
|
||||
Spawning no worker at all
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
All the workers do is loop forever and poll updates from a queue that is filled from the ``ReadThread``,
|
||||
responsible for reading every item off the network.
|
||||
If you only need a worker and the ``MainThread`` would be doing no other job,
|
||||
this is the preferred way. You can easily do the same as the workers like so:
|
||||
This is normal initialization (of course, pass session name, API ID and hash).
|
||||
Nothing we don't know already.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
while True:
|
||||
try:
|
||||
update = client.updates.poll()
|
||||
if not update:
|
||||
continue
|
||||
|
||||
print('I received', update)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
client.disconnect()
|
||||
|
||||
Note that ``poll`` accepts a ``timeout=`` parameter,
|
||||
and it will return ``None`` if other thread got the update before you could or if the timeout expired,
|
||||
so it's important to check ``if not update``.
|
||||
|
||||
This can coexist with the rest of ``N`` workers, or you can set it to ``0`` additional workers:
|
||||
|
||||
``client = TelegramClient('session', api_id, api_hash, update_workers=0)``
|
||||
|
||||
You **must** set it to ``0`` (or other number), as it defaults to ``None`` and there is a different.
|
||||
``None`` workers means updates won't be processed *at all*,
|
||||
so you must set it to some value (0 or greater) if you want ``client.updates.poll()`` to work.
|
||||
@client.on(events.NewMessage)
|
||||
|
||||
|
||||
Using the main thread instead the ``ReadThread``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have no work to do on the ``MainThread`` and you were planning to have a ``while True: sleep(1)``,
|
||||
don't do that. Instead, don't spawn the secondary ``ReadThread`` at all like so:
|
||||
This Python decorator will attach itself to the ``my_event_handler``
|
||||
definition, and basically means that *on* a ``NewMessage`` *event*,
|
||||
the callback function you're about to define will be called:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient(
|
||||
...
|
||||
spawn_read_thread=False
|
||||
)
|
||||
def my_event_handler(event):
|
||||
if 'hello' in event.raw_text:
|
||||
event.reply('hi!')
|
||||
|
||||
And then ``.idle()`` from the ``MainThread``:
|
||||
|
||||
``client.idle()``
|
||||
|
||||
You can stop it with :kbd:`Control+C`,
|
||||
and you can configure the signals to be used in a similar fashion to `Python Telegram Bot`__.
|
||||
|
||||
As a complete example:
|
||||
If a ``NewMessage`` event occurs, and ``'hello'`` is in the text of the
|
||||
message, we ``reply`` to the event with a ``'hi!'`` message.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def callback(update):
|
||||
print('I received', update)
|
||||
|
||||
client = TelegramClient('session', api_id, api_hash,
|
||||
update_workers=1, spawn_read_thread=False)
|
||||
|
||||
client.connect()
|
||||
client.add_update_handler(callback)
|
||||
client.idle() # ends with Ctrl+C
|
||||
client.disconnect()
|
||||
client.idle()
|
||||
|
||||
|
||||
Finally, this tells the client that we're done with our code, and want
|
||||
to listen for all these events to occur. Of course, you might want to
|
||||
do other things instead idling. For this refer to :ref:`update-modes`.
|
||||
|
||||
|
||||
More on events
|
||||
**************
|
||||
|
||||
The ``NewMessage`` event has much more than what was shown. You can access
|
||||
the ``.sender`` of the message through that member, or even see if the message
|
||||
had ``.media``, a ``.photo`` or a ``.document`` (which you could download with
|
||||
for example ``client.download_media(event.photo)``.
|
||||
|
||||
If you don't want to ``.reply`` as a reply, you can use the ``.respond()``
|
||||
method instead. Of course, there are more events such as ``ChatAction`` or
|
||||
``UserUpdate``, and they're all used in the same way. Simply add the
|
||||
``@client.on(events.XYZ)`` decorator on the top of your handler and you're
|
||||
done! The event that will be passed always is of type ``XYZ.Event`` (for
|
||||
instance, ``NewMessage.Event``), except for the ``Raw`` event which just
|
||||
passes the ``Update`` object.
|
||||
|
||||
Note that ``.reply()`` and ``.respond()`` are just wrappers around the
|
||||
``client.send_message()`` method which supports the ``file=`` parameter.
|
||||
This means you can reply with a photo if you do ``client.reply(file=photo)``.
|
||||
|
||||
You can put the same event on many handlers, and even different events on
|
||||
the same handler. You can also have a handler work on only specific chats,
|
||||
for example:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import ast
|
||||
import random
|
||||
|
||||
|
||||
# Either a single item or a list of them will work for the chats.
|
||||
# You can also use the IDs, Peers, or even User/Chat/Channel objects.
|
||||
@client.on(events.NewMessage(chats=('TelethonChat', 'TelethonOffTopic')))
|
||||
def normal_handler(event):
|
||||
if 'roll' in event.raw_text:
|
||||
event.reply(str(random.randint(1, 6)))
|
||||
|
||||
|
||||
# Similarly, you can use incoming=True for messages that you receive
|
||||
@client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True))
|
||||
def admin_handler(event):
|
||||
if event.raw_text.startswith('eval'):
|
||||
expression = event.raw_text.replace('eval', '').strip()
|
||||
event.reply(str(ast.literal_eval(expression)))
|
||||
|
||||
|
||||
You can pass one or more chats to the ``chats`` parameter (as a list or tuple),
|
||||
and only events from there will be processed. You can also specify whether you
|
||||
want to handle incoming or outgoing messages (those you receive or those you
|
||||
send). In this example, people can say ``'roll'`` and you will reply with a
|
||||
random number, while if you say ``'eval 4+4'``, you will reply with the
|
||||
solution. Try it!
|
||||
|
||||
|
||||
Events without decorators
|
||||
*************************
|
||||
|
||||
If for any reason you can't use the ``@client.on`` syntax, don't worry.
|
||||
You can call ``client.add_event_handler(callback, event)`` to achieve
|
||||
the same effect.
|
||||
|
||||
Similar to that method, you also have :meth:`client.remove_event_handler`
|
||||
and :meth:`client.list_event_handlers` which do as they names indicate.
|
||||
|
||||
The ``event`` type is optional in all methods and defaults to ``events.Raw``
|
||||
for adding, and ``None`` when removing (so all callbacks would be removed).
|
||||
|
||||
|
||||
Stopping propagation of Updates
|
||||
*******************************
|
||||
|
||||
There might be cases when an event handler is supposed to be used solitary and
|
||||
it makes no sense to process any other handlers in the chain. For this case,
|
||||
it is possible to raise a ``StopPropagation`` exception which will cause the
|
||||
propagation of the update through your handlers to stop:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.events import StopPropagation
|
||||
|
||||
@client.on(events.NewMessage)
|
||||
def _(event):
|
||||
# ... some conditions
|
||||
event.delete()
|
||||
|
||||
# Other handlers won't have an event to work with
|
||||
raise StopPropagation
|
||||
|
||||
@client.on(events.NewMessage)
|
||||
def _(event):
|
||||
# Will never be reached, because it is the second handler
|
||||
# in the chain.
|
||||
pass
|
||||
|
||||
|
||||
Remember to check :ref:`telethon-events-package` if you're looking for
|
||||
the methods reference.
|
||||
|
||||
|
||||
__ https://python-telegram-bot.org/
|
||||
__ https://lonamiwebs.github.io/Telethon/types/update.html
|
||||
__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460
|
1726
readthedocs/extra/changelog.rst
Normal file
1726
readthedocs/extra/changelog.rst
Normal file
File diff suppressed because it is too large
Load Diff
54
readthedocs/extra/developing/api-status.rst
Normal file
54
readthedocs/extra/developing/api-status.rst
Normal file
|
@ -0,0 +1,54 @@
|
|||
.. _api-status:
|
||||
|
||||
==========
|
||||
API Status
|
||||
==========
|
||||
|
||||
|
||||
In an attempt to help everyone who works with the Telegram API, the
|
||||
library will by default report all *Remote Procedure Call* errors to
|
||||
`RPC PWRTelegram <https://rpc.pwrtelegram.xyz/>`__, a public database
|
||||
anyone can query, made by `Daniil <https://github.com/danog>`__. All the
|
||||
information sent is a ``GET`` request with the error code, error message
|
||||
and method used.
|
||||
|
||||
If you still would like to opt out, you can disable this feature by setting
|
||||
``client.session.report_errors = False``. However Daniil would really thank
|
||||
you if you helped him (and everyone) by keeping it on!
|
||||
|
||||
Querying the API status
|
||||
***********************
|
||||
|
||||
The API is accessed through ``GET`` requests, which can be made for
|
||||
instance through ``curl``. A JSON response will be returned.
|
||||
|
||||
**All known errors and their description**:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl https://rpc.pwrtelegram.xyz/?all
|
||||
|
||||
**Error codes for a specific request**:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl https://rpc.pwrtelegram.xyz/?for=messages.sendMessage
|
||||
|
||||
**Number of** ``RPC_CALL_FAIL``:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl https://rpc.pwrtelegram.xyz/?rip # last hour
|
||||
curl https://rpc.pwrtelegram.xyz/?rip=$(time()-60) # last minute
|
||||
|
||||
**Description of errors**:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl https://rpc.pwrtelegram.xyz/?description_for=SESSION_REVOKED
|
||||
|
||||
**Code of a specific error**:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl https://rpc.pwrtelegram.xyz/?code_for=STICKERSET_INVALID
|
22
readthedocs/extra/developing/coding-style.rst
Normal file
22
readthedocs/extra/developing/coding-style.rst
Normal file
|
@ -0,0 +1,22 @@
|
|||
============
|
||||
Coding Style
|
||||
============
|
||||
|
||||
|
||||
Basically, make it **readable**, while keeping the style similar to the
|
||||
code of whatever file you're working on.
|
||||
|
||||
Also note that not everyone has 4K screens for their primary monitors,
|
||||
so please try to stick to the 80-columns limit. This makes it easy to
|
||||
``git diff`` changes from a terminal before committing changes. If the
|
||||
line has to be long, please don't exceed 120 characters.
|
||||
|
||||
For the commit messages, please make them *explanatory*. Not only
|
||||
they're helpful to troubleshoot when certain issues could have been
|
||||
introduced, but they're also used to construct the change log once a new
|
||||
version is ready.
|
||||
|
||||
If you don't know enough Python, I strongly recommend reading `Dive Into
|
||||
Python 3 <http://www.diveintopython3.net/>`__, available online for
|
||||
free. For instance, remember to do ``if x is None`` or
|
||||
``if x is not None`` instead ``if x == None``!
|
25
readthedocs/extra/developing/philosophy.rst
Normal file
25
readthedocs/extra/developing/philosophy.rst
Normal file
|
@ -0,0 +1,25 @@
|
|||
==========
|
||||
Philosophy
|
||||
==========
|
||||
|
||||
|
||||
The intention of the library is to have an existing MTProto library
|
||||
existing with hardly any dependencies (indeed, wherever Python is
|
||||
available, you can run this library).
|
||||
|
||||
Being written in Python means that performance will be nowhere close to
|
||||
other implementations written in, for instance, Java, C++, Rust, or
|
||||
pretty much any other compiled language. However, the library turns out
|
||||
to actually be pretty decent for common operations such as sending
|
||||
messages, receiving updates, or other scripting. Uploading files may be
|
||||
notably slower, but if you would like to contribute, pull requests are
|
||||
appreciated!
|
||||
|
||||
If ``libssl`` is available on your system, the library will make use of
|
||||
it to speed up some critical parts such as encrypting and decrypting the
|
||||
messages. Files will notably be sent and downloaded faster.
|
||||
|
||||
The main focus is to keep everything clean and simple, for everyone to
|
||||
understand how working with MTProto and Telegram works. Don't be afraid
|
||||
to read the source, the code won't bite you! It may prove useful when
|
||||
using the library on your own use cases.
|
43
readthedocs/extra/developing/project-structure.rst
Normal file
43
readthedocs/extra/developing/project-structure.rst
Normal file
|
@ -0,0 +1,43 @@
|
|||
=================
|
||||
Project Structure
|
||||
=================
|
||||
|
||||
|
||||
Main interface
|
||||
**************
|
||||
|
||||
The library itself is under the ``telethon/`` directory. The
|
||||
``__init__.py`` file there exposes the main ``TelegramClient``, a class
|
||||
that servers as a nice interface with the most commonly used methods on
|
||||
Telegram such as sending messages, retrieving the message history,
|
||||
handling updates, etc.
|
||||
|
||||
The ``TelegramClient`` inherits the ``TelegramBareClient``. The later is
|
||||
basically a pruned version of the ``TelegramClient``, which knows basic
|
||||
stuff like ``.invoke()``\ 'ing requests, downloading files, or switching
|
||||
between data centers. This is primary to keep the method count per class
|
||||
and file low and manageable.
|
||||
|
||||
Both clients make use of the ``network/mtproto_sender.py``. The
|
||||
``MtProtoSender`` class handles packing requests with the ``salt``,
|
||||
``id``, ``sequence``, etc., and also handles how to process responses
|
||||
(i.e. pong, RPC errors). This class communicates through Telegram via
|
||||
its ``.connection`` member.
|
||||
|
||||
The ``Connection`` class uses a ``extensions/tcp_client``, a C#-like
|
||||
``TcpClient`` to ease working with sockets in Python. All the
|
||||
``TcpClient`` know is how to connect through TCP and writing/reading
|
||||
from the socket with optional cancel.
|
||||
|
||||
The ``Connection`` class bundles up all the connections modes and sends
|
||||
and receives the messages accordingly (TCP full, obfuscated,
|
||||
intermediate…).
|
||||
|
||||
Auto-generated code
|
||||
*******************
|
||||
|
||||
The files under ``telethon_generator/`` are used to generate the code
|
||||
that gets placed under ``telethon/tl/``. The ``TLGenerator`` takes in a
|
||||
``.tl`` file, and spits out the generated classes which represent, as
|
||||
Python classes, the request and types defined in the ``.tl`` file. It
|
||||
also constructs an index so that they can be imported easily.
|
|
@ -0,0 +1,73 @@
|
|||
===============================
|
||||
Telegram API in Other Languages
|
||||
===============================
|
||||
|
||||
|
||||
Telethon was made for **Python**, and as far as I know, there is no
|
||||
*exact* port to other languages. However, there *are* other
|
||||
implementations made by awesome people (one needs to be awesome to
|
||||
understand the official Telegram documentation) on several languages
|
||||
(even more Python too), listed below:
|
||||
|
||||
C
|
||||
*
|
||||
|
||||
Possibly the most well-known unofficial open source implementation out
|
||||
there by `@vysheng <https://github.com/vysheng>`__,
|
||||
`tgl <https://github.com/vysheng/tgl>`__, and its console client
|
||||
`telegram-cli <https://github.com/vysheng/tg>`__. Latest development
|
||||
has been moved to `BitBucket <https://bitbucket.org/vysheng/tdcli>`__.
|
||||
|
||||
C++
|
||||
***
|
||||
|
||||
The newest (and official) library, written from scratch, is called
|
||||
`tdlib <https://github.com/tdlib/td>`__ and is what the Telegram X
|
||||
uses. You can find more information in the official documentation,
|
||||
published `here <https://core.telegram.org/tdlib/docs/>`__.
|
||||
|
||||
JavaScript
|
||||
**********
|
||||
|
||||
`@zerobias <https://github.com/zerobias>`__ is working on
|
||||
`telegram-mtproto <https://github.com/zerobias/telegram-mtproto>`__,
|
||||
a work-in-progress JavaScript library installable via
|
||||
`npm <https://www.npmjs.com/>`__.
|
||||
|
||||
Kotlin
|
||||
******
|
||||
|
||||
`Kotlogram <https://github.com/badoualy/kotlogram>`__ is a Telegram
|
||||
implementation written in Kotlin (one of the
|
||||
`official <https://blog.jetbrains.com/kotlin/2017/05/kotlin-on-android-now-official/>`__
|
||||
languages for
|
||||
`Android <https://developer.android.com/kotlin/index.html>`__) by
|
||||
`@badoualy <https://github.com/badoualy>`__, currently as a beta–
|
||||
yet working.
|
||||
|
||||
PHP
|
||||
***
|
||||
|
||||
A PHP implementation is also available thanks to
|
||||
`@danog <https://github.com/danog>`__ and his
|
||||
`MadelineProto <https://github.com/danog/MadelineProto>`__ project, with
|
||||
a very nice `online
|
||||
documentation <https://daniil.it/MadelineProto/API_docs/>`__ too.
|
||||
|
||||
Python
|
||||
******
|
||||
|
||||
A fairly new (as of the end of 2017) Telegram library written from the
|
||||
ground up in Python by
|
||||
`@delivrance <https://github.com/delivrance>`__ and his
|
||||
`Pyrogram <https://github.com/pyrogram/pyrogram>`__ library.
|
||||
There isn't really a reason to pick it over Telethon and it'd be kinda
|
||||
sad to see you go, but it would be nice to know what you miss from each
|
||||
other library in either one so both can improve.
|
||||
|
||||
Rust
|
||||
****
|
||||
|
||||
Yet another work-in-progress implementation, this time for Rust thanks
|
||||
to `@JuanPotato <https://github.com/JuanPotato>`__ under the fancy
|
||||
name of `Vail <https://github.com/JuanPotato/Vail>`__.
|
35
readthedocs/extra/developing/test-servers.rst
Normal file
35
readthedocs/extra/developing/test-servers.rst
Normal file
|
@ -0,0 +1,35 @@
|
|||
============
|
||||
Test Servers
|
||||
============
|
||||
|
||||
|
||||
To run Telethon on a test server, use the following code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient(None, api_id, api_hash)
|
||||
client.session.set_dc(dc_id, '149.154.167.40', 80)
|
||||
|
||||
You can check your ``'test ip'`` on https://my.telegram.org.
|
||||
|
||||
You should set ``None`` session so to ensure you're generating a new
|
||||
authorization key for it (it would fail if you used a session where you
|
||||
had previously connected to another data center).
|
||||
|
||||
Note that port 443 might not work, so you can try with 80 instead.
|
||||
|
||||
Once you're connected, you'll likely be asked to either sign in or sign up.
|
||||
Remember `anyone can access the phone you
|
||||
choose <https://core.telegram.org/api/datacenter#testing-redirects>`__,
|
||||
so don't store sensitive data here.
|
||||
|
||||
Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and
|
||||
``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would
|
||||
be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five
|
||||
times, in this case, ``22222`` so we can hardcode that:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient(None, api_id, api_hash)
|
||||
client.session.set_dc(2, '149.154.167.40', 80)
|
||||
client.start(phone='9996621234', code_callback=lambda: '22222')
|
|
@ -0,0 +1,17 @@
|
|||
============================
|
||||
Tips for Porting the Project
|
||||
============================
|
||||
|
||||
|
||||
If you're going to use the code on this repository to guide you, please
|
||||
be kind and don't forget to mention it helped you!
|
||||
|
||||
You should start by reading the source code on the `first
|
||||
release <https://github.com/LonamiWebs/Telethon/releases/tag/v0.1>`__ of
|
||||
the project, and start creating a ``MtProtoSender``. Once this is made,
|
||||
you should write by hand the code to authenticate on the Telegram's
|
||||
server, which are some steps required to get the key required to talk to
|
||||
them. Save it somewhere! Then, simply mimic, or reinvent other parts of
|
||||
the code, and it will be ready to go within a few days.
|
||||
|
||||
Good luck!
|
|
@ -0,0 +1,33 @@
|
|||
===============================
|
||||
Understanding the Type Language
|
||||
===============================
|
||||
|
||||
|
||||
`Telegram's Type Language <https://core.telegram.org/mtproto/TL>`__
|
||||
(also known as TL, found on ``.tl`` files) is a concise way to define
|
||||
what other programming languages commonly call classes or structs.
|
||||
|
||||
Every definition is written as follows for a Telegram object is defined
|
||||
as follows:
|
||||
|
||||
``name#id argument_name:argument_type = CommonType``
|
||||
|
||||
This means that in a single line you know what the ``TLObject`` name is.
|
||||
You know it's unique ID, and you know what arguments it has. It really
|
||||
isn't that hard to write a generator for generating code to any
|
||||
platform!
|
||||
|
||||
The generated code should also be able to *encode* the ``TLObject`` (let
|
||||
this be a request or a type) into bytes, so they can be sent over the
|
||||
network. This isn't a big deal either, because you know how the
|
||||
``TLObject``\ 's are made, and how the types should be serialized.
|
||||
|
||||
You can either write your own code generator, or use the one this
|
||||
library provides, but please be kind and keep some special mention to
|
||||
this project for helping you out.
|
||||
|
||||
This is only a introduction. The ``TL`` language is not *that* easy. But
|
||||
it's not that hard either. You're free to sniff the
|
||||
``telethon_generator/`` files and learn how to parse other more complex
|
||||
lines, such as ``flags`` (to indicate things that may or may not be
|
||||
written at all) and ``vector``\ 's.
|
|
@ -1,13 +1,19 @@
|
|||
======
|
||||
====
|
||||
Bots
|
||||
======
|
||||
====
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
||||
|
||||
|
||||
Talking to Inline Bots
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
**********************
|
||||
|
||||
You can query an inline bot, such as `@VoteBot`__
|
||||
(note, *query*, not *interact* with a voting message), by making use of
|
||||
the `GetInlineBotResultsRequest`__ request:
|
||||
You can query an inline bot, such as `@VoteBot`__ (note, *query*,
|
||||
not *interact* with a voting message), by making use of the
|
||||
`GetInlineBotResultsRequest`__ request:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -32,11 +38,10 @@ And you can select any of their results by using
|
|||
|
||||
|
||||
Talking to Bots with special reply markup
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
*****************************************
|
||||
|
||||
To interact with a message that has a special reply markup, such as
|
||||
`@VoteBot`__ polls, you would use
|
||||
`GetBotCallbackAnswerRequest`__:
|
||||
`@VoteBot`__ polls, you would use `GetBotCallbackAnswerRequest`__:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -48,7 +53,7 @@ To interact with a message that has a special reply markup, such as
|
|||
data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data
|
||||
))
|
||||
|
||||
It’s a bit verbose, but it has all the information you would need to
|
||||
It's a bit verbose, but it has all the information you would need to
|
||||
show it visually (button rows, and buttons within each row, each with
|
||||
its own data).
|
||||
|
||||
|
@ -56,4 +61,4 @@ __ https://t.me/vote
|
|||
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_inline_bot_results.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/send_inline_bot_result.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_bot_callback_answer.html
|
||||
__ https://t.me/vote
|
||||
__ https://t.me/vote
|
256
readthedocs/extra/examples/chats-and-channels.rst
Normal file
256
readthedocs/extra/examples/chats-and-channels.rst
Normal file
|
@ -0,0 +1,256 @@
|
|||
===============================
|
||||
Working with Chats and Channels
|
||||
===============================
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
||||
|
||||
|
||||
Joining a chat or channel
|
||||
*************************
|
||||
|
||||
Note that :tl:`Chat` are normal groups, and :tl:`Channel` are a
|
||||
special form of ``Chat``, which can also be super-groups if
|
||||
their ``megagroup`` member is ``True``.
|
||||
|
||||
|
||||
Joining a public channel
|
||||
************************
|
||||
|
||||
Once you have the :ref:`entity <entities>` of the channel you want to join
|
||||
to, you can make use of the `JoinChannelRequest`__ to join such channel:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.channels import JoinChannelRequest
|
||||
client(JoinChannelRequest(channel))
|
||||
|
||||
# In the same way, you can also leave such channel
|
||||
from telethon.tl.functions.channels import LeaveChannelRequest
|
||||
client(LeaveChannelRequest(input_channel))
|
||||
|
||||
|
||||
For more on channels, check the `channels namespace`__.
|
||||
|
||||
|
||||
Joining a private chat or channel
|
||||
*********************************
|
||||
|
||||
If all you have is a link like this one:
|
||||
``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have
|
||||
enough information to join! The part after the
|
||||
``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this
|
||||
example, is the ``hash`` of the chat or channel. Now you can use
|
||||
`ImportChatInviteRequest`__ as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import ImportChatInviteRequest
|
||||
updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))
|
||||
|
||||
|
||||
Adding someone else to such chat or channel
|
||||
*******************************************
|
||||
|
||||
If you don't want to add yourself, maybe because you're already in,
|
||||
you can always add someone else with the `AddChatUserRequest`__, which
|
||||
use is very straightforward, or `InviteToChannelRequest`__ for channels:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# For normal chats
|
||||
from telethon.tl.functions.messages import AddChatUserRequest
|
||||
|
||||
client(AddChatUserRequest(
|
||||
chat_id,
|
||||
user_to_add,
|
||||
fwd_limit=10 # Allow the user to see the 10 last messages
|
||||
))
|
||||
|
||||
# For channels
|
||||
from telethon.tl.functions.channels import InviteToChannelRequest
|
||||
|
||||
client(InviteToChannelRequest(
|
||||
channel,
|
||||
[users_to_add]
|
||||
))
|
||||
|
||||
|
||||
|
||||
Checking a link without joining
|
||||
*******************************
|
||||
|
||||
If you don't need to join but rather check whether it's a group or a
|
||||
channel, you can use the `CheckChatInviteRequest`__, which takes in
|
||||
the hash of said channel or group.
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/chat.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/chat.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/invite_to_channel.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html
|
||||
|
||||
|
||||
Retrieving all chat members (channels too)
|
||||
******************************************
|
||||
|
||||
You can use
|
||||
`client.get_participants <telethon.telegram_client.TelegramClient.get_participants>`
|
||||
to retrieve the participants (click it to see the relevant parameters).
|
||||
Most of the time you will just need ``client.get_participants(entity)``.
|
||||
|
||||
This is what said method is doing behind the scenes as an example.
|
||||
|
||||
In order to get all the members from a mega-group or channel, you need
|
||||
to use `GetParticipantsRequest`__. As we can see it needs an
|
||||
`InputChannel`__, (passing the mega-group or channel you're going to
|
||||
use will work), and a mandatory `ChannelParticipantsFilter`__. The
|
||||
closest thing to "no filter" is to simply use
|
||||
`ChannelParticipantsSearch`__ with an empty ``'q'`` string.
|
||||
|
||||
If we want to get *all* the members, we need to use a moving offset and
|
||||
a fixed limit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.channels import GetParticipantsRequest
|
||||
from telethon.tl.types import ChannelParticipantsSearch
|
||||
from time import sleep
|
||||
|
||||
offset = 0
|
||||
limit = 100
|
||||
all_participants = []
|
||||
|
||||
while True:
|
||||
participants = client(GetParticipantsRequest(
|
||||
channel, ChannelParticipantsSearch(''), offset, limit,
|
||||
hash=0
|
||||
))
|
||||
if not participants.users:
|
||||
break
|
||||
all_participants.extend(participants.users)
|
||||
offset += len(participants.users)
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
If you need more than 10,000 members from a group you should use the
|
||||
mentioned ``client.get_participants(..., aggressive=True)``. It will
|
||||
do some tricks behind the scenes to get as many entities as possible.
|
||||
Refer to `issue 573`__ for more on this.
|
||||
|
||||
|
||||
Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__,
|
||||
which may have more information you need (like the role of the
|
||||
participants, total count of members, etc.)
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/573
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html
|
||||
|
||||
|
||||
Recent Actions
|
||||
**************
|
||||
|
||||
"Recent actions" is simply the name official applications have given to
|
||||
the "admin log". Simply use `GetAdminLogRequest`__ for that, and
|
||||
you'll get AdminLogResults.events in return which in turn has the final
|
||||
`.action`__.
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html
|
||||
|
||||
|
||||
Admin Permissions
|
||||
*****************
|
||||
|
||||
Giving or revoking admin permissions can be done with the `EditAdminRequest`__:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.channels import EditAdminRequest
|
||||
from telethon.tl.types import ChannelAdminRights
|
||||
|
||||
# You need both the channel and who to grant permissions
|
||||
# They can either be channel/user or input channel/input user.
|
||||
#
|
||||
# ChannelAdminRights is a list of granted permissions.
|
||||
# Set to True those you want to give.
|
||||
rights = ChannelAdminRights(
|
||||
post_messages=None,
|
||||
add_admins=None,
|
||||
invite_users=None,
|
||||
change_info=True,
|
||||
ban_users=None,
|
||||
delete_messages=True,
|
||||
pin_messages=True,
|
||||
invite_link=None,
|
||||
edit_messages=None
|
||||
)
|
||||
# Equivalent to:
|
||||
# rights = ChannelAdminRights(
|
||||
# change_info=True,
|
||||
# delete_messages=True,
|
||||
# pin_messages=True
|
||||
# )
|
||||
|
||||
# Once you have a ChannelAdminRights, invoke it
|
||||
client(EditAdminRequest(channel, user, rights))
|
||||
|
||||
# User will now be able to change group info, delete other people's
|
||||
# messages and pin messages.
|
||||
|
||||
| Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all
|
||||
| parameters to ``True`` to give a user full permissions, as not all
|
||||
| permissions are related to both broadcast channels/megagroups.
|
||||
|
|
||||
| E.g. trying to set ``post_messages=True`` in a megagroup will raise an
|
||||
| error. It is recommended to always use keyword arguments, and to set only
|
||||
| the permissions the user needs. If you don't need to change a permission,
|
||||
| it can be omitted (full list `here`__).
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/edit_admin.html
|
||||
__ https://github.com/Kyle2142
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/490
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel_admin_rights.html
|
||||
|
||||
|
||||
Increasing View Count in a Channel
|
||||
**********************************
|
||||
|
||||
It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
|
||||
while I don't understand why so many people ask this, the solution is to
|
||||
use `GetMessagesViewsRequest`__, setting ``increment=True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
# Obtain `channel' through dialogs or through client.get_entity() or anyhow.
|
||||
# Obtain `msg_ids' through `.get_message_history()` or anyhow. Must be a list.
|
||||
|
||||
client(GetMessagesViewsRequest(
|
||||
peer=channel,
|
||||
id=msg_ids,
|
||||
increment=True
|
||||
))
|
||||
|
||||
|
||||
Note that you can only do this **once or twice a day** per account,
|
||||
running this in a loop will obviously not increase the views forever
|
||||
unless you wait a day between each iteration. If you run it any sooner
|
||||
than that, the views simply won't be increased.
|
||||
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/233
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/305
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/409
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/447
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html
|
134
readthedocs/extra/examples/working-with-messages.rst
Normal file
134
readthedocs/extra/examples/working-with-messages.rst
Normal file
|
@ -0,0 +1,134 @@
|
|||
=====================
|
||||
Working with messages
|
||||
=====================
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
||||
|
||||
|
||||
Forwarding messages
|
||||
*******************
|
||||
|
||||
This request is available as a friendly method through
|
||||
`client.forward_messages <telethon.telegram_client.TelegramClient.forward_messages>`,
|
||||
and can be used like shown below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# If you only have the message IDs
|
||||
client.forward_messages(
|
||||
entity, # to which entity you are forwarding the messages
|
||||
message_ids, # the IDs of the messages (or message) to forward
|
||||
from_entity # who sent the messages?
|
||||
)
|
||||
|
||||
# If you have ``Message`` objects
|
||||
client.forward_messages(
|
||||
entity, # to which entity you are forwarding the messages
|
||||
messages # the messages (or message) to forward
|
||||
)
|
||||
|
||||
# You can also do it manually if you prefer
|
||||
from telethon.tl.functions.messages import ForwardMessagesRequest
|
||||
|
||||
messages = foo() # retrieve a few messages (or even one, in a list)
|
||||
from_entity = bar()
|
||||
to_entity = baz()
|
||||
|
||||
client(ForwardMessagesRequest(
|
||||
from_peer=from_entity, # who sent these messages?
|
||||
id=[msg.id for msg in messages], # which are the messages?
|
||||
to_peer=to_entity # who are we forwarding them to?
|
||||
))
|
||||
|
||||
The named arguments are there for clarity, although they're not needed because
|
||||
they appear in order. You can obviously just wrap a single message on the list
|
||||
too, if that's all you have.
|
||||
|
||||
|
||||
Searching Messages
|
||||
*******************
|
||||
|
||||
Messages are searched through the obvious SearchRequest_, but you may run
|
||||
into issues_. A valid example would be:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import SearchRequest
|
||||
from telethon.tl.types import InputMessagesFilterEmpty
|
||||
|
||||
filter = InputMessagesFilterEmpty()
|
||||
result = client(SearchRequest(
|
||||
peer=peer, # On which chat/conversation
|
||||
q='query', # What to search for
|
||||
filter=filter, # Filter to use (maybe filter for media)
|
||||
min_date=None, # Minimum date
|
||||
max_date=None, # Maximum date
|
||||
offset_id=0, # ID of the message to use as offset
|
||||
add_offset=0, # Additional offset
|
||||
limit=10, # How many results
|
||||
max_id=0, # Maximum message ID
|
||||
min_id=0, # Minimum message ID
|
||||
from_id=None # Who must have sent the message (peer)
|
||||
))
|
||||
|
||||
It's important to note that the optional parameter ``from_id`` could have
|
||||
been omitted (defaulting to ``None``). Changing it to InputUserEmpty_, as one
|
||||
could think to specify "no user", won't work because this parameter is a flag,
|
||||
and it being unspecified has a different meaning.
|
||||
|
||||
If one were to set ``from_id=InputUserEmpty()``, it would filter messages
|
||||
from "empty" senders, which would likely match no users.
|
||||
|
||||
If you get a ``ChatAdminRequiredError`` on a channel, it's probably because
|
||||
you tried setting the ``from_id`` filter, and as the error says, you can't
|
||||
do that. Leave it set to ``None`` and it should work.
|
||||
|
||||
As with every method, make sure you use the right ID/hash combination for
|
||||
your ``InputUser`` or ``InputChat``, or you'll likely run into errors like
|
||||
``UserIdInvalidError``.
|
||||
|
||||
|
||||
Sending stickers
|
||||
****************
|
||||
|
||||
Stickers are nothing else than ``files``, and when you successfully retrieve
|
||||
the stickers for a certain sticker set, all you will have are ``handles`` to
|
||||
these files. Remember, the files Telegram holds on their servers can be
|
||||
referenced through this pair of ID/hash (unique per user), and you need to
|
||||
use this handle when sending a "document" message. This working example will
|
||||
send yourself the very first sticker you have:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Get all the sticker sets this user has
|
||||
sticker_sets = client(GetAllStickersRequest(0))
|
||||
|
||||
# Choose a sticker set
|
||||
sticker_set = sticker_sets.sets[0]
|
||||
|
||||
# Get the stickers for this sticker set
|
||||
stickers = client(GetStickerSetRequest(
|
||||
stickerset=InputStickerSetID(
|
||||
id=sticker_set.id, access_hash=sticker_set.access_hash
|
||||
)
|
||||
))
|
||||
|
||||
# Stickers are nothing more than files, so send that
|
||||
client(SendMediaRequest(
|
||||
peer=client.get_me(),
|
||||
media=InputMediaDocument(
|
||||
id=InputDocument(
|
||||
id=stickers.documents[0].id,
|
||||
access_hash=stickers.documents[0].access_hash
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
.. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html
|
||||
.. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html
|
||||
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215
|
||||
.. _InputUserEmpty: https://lonamiwebs.github.io/Telethon/constructors/input_user_empty.html
|
|
@ -1,6 +1,6 @@
|
|||
=========================================
|
||||
========================================
|
||||
Deleted, Limited or Deactivated Accounts
|
||||
=========================================
|
||||
========================================
|
||||
|
||||
If you're from Iran or Russian, we have bad news for you.
|
||||
Telegram is much more likely to ban these numbers,
|
||||
|
@ -23,4 +23,4 @@ For more discussion, please see `issue 297`__.
|
|||
|
||||
|
||||
__ https://t.me/SpamBot
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/297
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/297
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
================
|
||||
Enable Logging
|
||||
Enabling Logging
|
||||
================
|
||||
|
||||
Telethon makes use of the `logging`__ module, and you can enable it as follows:
|
||||
|
||||
.. code-block:: python
|
||||
.. code:: python
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
You can also use it in your own project very easily:
|
||||
The library has the `NullHandler`__ added by default so that no log calls
|
||||
will be printed unless you explicitly enable it.
|
||||
|
||||
You can also `use the module`__ on your own project very easily:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -21,4 +24,17 @@ You can also use it in your own project very easily:
|
|||
logger.warning('This is a warning!')
|
||||
|
||||
|
||||
__ https://docs.python.org/3/library/logging.html
|
||||
If you want to enable ``logging`` for your project *but* use a different
|
||||
log level for the library:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
# For instance, show only warnings and above
|
||||
logging.getLogger('telethon').setLevel(level=logging.WARNING)
|
||||
|
||||
|
||||
__ https://docs.python.org/3/library/logging.html
|
||||
__ https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library
|
||||
__ https://docs.python.org/3/howto/logging.html
|
||||
|
|
|
@ -2,26 +2,28 @@
|
|||
RPC Errors
|
||||
==========
|
||||
|
||||
RPC stands for Remote Procedure Call, and when Telethon raises an
|
||||
``RPCError``, it’s most likely because you have invoked some of the API
|
||||
RPC stands for Remote Procedure Call, and when the library raises
|
||||
a ``RPCError``, it's because you have invoked some of the API
|
||||
methods incorrectly (wrong parameters, wrong permissions, or even
|
||||
something went wrong on Telegram’s server). The most common are:
|
||||
something went wrong on Telegram's server). All the errors are
|
||||
available in :ref:`telethon-errors-package`, but some examples are:
|
||||
|
||||
- ``FloodError`` (420), the same request was repeated many times. Must
|
||||
wait ``.seconds``.
|
||||
- ``FloodWaitError`` (420), the same request was repeated many times.
|
||||
Must wait ``.seconds`` (you can access this parameter).
|
||||
- ``SessionPasswordNeededError``, if you have setup two-steps
|
||||
verification on Telegram.
|
||||
- ``CdnFileTamperedError``, if the media you were trying to download
|
||||
from a CDN has been altered.
|
||||
- ``ChatAdminRequiredError``, you don’t have permissions to perform
|
||||
- ``ChatAdminRequiredError``, you don't have permissions to perform
|
||||
said operation on a chat or channel. Try avoiding filters, i.e. when
|
||||
searching messages.
|
||||
|
||||
The generic classes for different error codes are: \* ``InvalidDCError``
|
||||
(303), the request must be repeated on another DC. \*
|
||||
``BadRequestError`` (400), the request contained errors. \*
|
||||
``UnauthorizedError`` (401), the user is not authorized yet. \*
|
||||
``ForbiddenError`` (403), privacy violation error. \* ``NotFoundError``
|
||||
(404), make sure you’re invoking ``Request``\ ’s!
|
||||
The generic classes for different error codes are:
|
||||
|
||||
If the error is not recognised, it will only be an ``RPCError``.
|
||||
- ``InvalidDCError`` (303), the request must be repeated on another DC.
|
||||
- ``BadRequestError`` (400), the request contained errors.
|
||||
- ``UnauthorizedError`` (401), the user is not authorized yet.
|
||||
- ``ForbiddenError`` (403), privacy violation error.
|
||||
- ``NotFoundError`` (404), make sure you're invoking ``Request``\ 's!
|
||||
|
||||
If the error is not recognised, it will only be an ``RPCError``.
|
||||
|
|
63
readthedocs/extra/wall-of-shame.rst
Normal file
63
readthedocs/extra/wall-of-shame.rst
Normal file
|
@ -0,0 +1,63 @@
|
|||
=============
|
||||
Wall of Shame
|
||||
=============
|
||||
|
||||
|
||||
This project has an
|
||||
`issues <https://github.com/LonamiWebs/Telethon/issues>`__ section for
|
||||
you to file **issues** whenever you encounter any when working with the
|
||||
library. Said section is **not** for issues on *your* program but rather
|
||||
issues with Telethon itself.
|
||||
|
||||
If you have not made the effort to 1. read through the docs and 2.
|
||||
`look for the method you need <https://lonamiwebs.github.io/Telethon/>`__,
|
||||
you will end up on the `Wall of
|
||||
Shame <https://github.com/LonamiWebs/Telethon/issues?q=is%3Aissue+label%3ARTFM+is%3Aclosed>`__,
|
||||
i.e. all issues labeled
|
||||
`"RTFM" <http://www.urbandictionary.com/define.php?term=RTFM>`__:
|
||||
|
||||
**rtfm**
|
||||
Literally "Read The F--king Manual"; a term showing the
|
||||
frustration of being bothered with questions so trivial that the asker
|
||||
could have quickly figured out the answer on their own with minimal
|
||||
effort, usually by reading readily-available documents. People who
|
||||
say"RTFM!" might be considered rude, but the true rude ones are the
|
||||
annoying people who take absolutely no self-responibility and expect to
|
||||
have all the answers handed to them personally.
|
||||
|
||||
*"Damn, that's the twelveth time that somebody posted this question
|
||||
to the messageboard today! RTFM, already!"*
|
||||
|
||||
*by Bill M. July 27, 2004*
|
||||
|
||||
If you have indeed read the docs, and have tried looking for the method,
|
||||
and yet you didn't find what you need, **that's fine**. Telegram's API
|
||||
can have some obscure names at times, and for this reason, there is a
|
||||
`"question"
|
||||
label <https://github.com/LonamiWebs/Telethon/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20label%3Aquestion%20>`__
|
||||
with questions that are okay to ask. Just state what you've tried so
|
||||
that we know you've made an effort, or you'll go to the Wall of Shame.
|
||||
|
||||
Of course, if the issue you're going to open is not even a question but
|
||||
a real issue with the library (thankfully, most of the issues have been
|
||||
that!), you won't end up here. Don't worry.
|
||||
|
||||
Current winner
|
||||
--------------
|
||||
|
||||
The current winner is `issue
|
||||
213 <https://github.com/LonamiWebs/Telethon/issues/213>`__:
|
||||
|
||||
**Issue:**
|
||||
|
||||
.. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg
|
||||
:alt: Winner issue
|
||||
|
||||
Winner issue
|
||||
|
||||
**Answer:**
|
||||
|
||||
.. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg
|
||||
:alt: Winner issue answer
|
||||
|
||||
Winner issue answer
|
|
@ -3,11 +3,29 @@
|
|||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
====================================
|
||||
Welcome to Telethon's documentation!
|
||||
====================================
|
||||
|
||||
Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.github.io/Telethon>`_.
|
||||
|
||||
Pure Python 3 Telegram client library.
|
||||
Official Site `here <https://lonamiwebs.github.io/Telethon>`_.
|
||||
Please follow the links on the index below to navigate from here,
|
||||
or use the menu on the left. Remember to read the :ref:`changelog`
|
||||
when you upgrade!
|
||||
|
||||
.. important::
|
||||
If you're new here, you want to read :ref:`getting-started`. If you're
|
||||
looking for the method reference, you should check :ref:`telethon-package`.
|
||||
|
||||
|
||||
What is this?
|
||||
*************
|
||||
|
||||
Telegram is a popular messaging application. This library is meant
|
||||
to make it easy for you to write Python programs that can interact
|
||||
with Telegram. Think of it as a wrapper that has already done the
|
||||
heavy job for you, so you can focus on developing an application.
|
||||
|
||||
|
||||
.. _installation-and-usage:
|
||||
|
@ -19,10 +37,9 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
|
|||
extra/basic/getting-started
|
||||
extra/basic/installation
|
||||
extra/basic/creating-a-client
|
||||
extra/basic/sessions
|
||||
extra/basic/sending-requests
|
||||
extra/basic/telegram-client
|
||||
extra/basic/entities
|
||||
extra/basic/working-with-updates
|
||||
extra/basic/accessing-the-full-api
|
||||
|
||||
|
||||
.. _Advanced-usage:
|
||||
|
@ -31,11 +48,20 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
|
|||
:maxdepth: 2
|
||||
:caption: Advanced Usage
|
||||
|
||||
extra/advanced
|
||||
extra/advanced-usage/signing-in
|
||||
extra/advanced-usage/working-with-messages
|
||||
extra/advanced-usage/users-and-chats
|
||||
extra/advanced-usage/bots
|
||||
extra/advanced-usage/accessing-the-full-api
|
||||
extra/advanced-usage/sessions
|
||||
extra/advanced-usage/update-modes
|
||||
|
||||
|
||||
.. _Examples:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Examples
|
||||
|
||||
extra/examples/working-with-messages
|
||||
extra/examples/chats-and-channels
|
||||
extra/examples/bots
|
||||
|
||||
|
||||
.. _Troubleshooting:
|
||||
|
@ -49,12 +75,36 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
|
|||
extra/troubleshooting/rpc-errors
|
||||
|
||||
|
||||
.. _Developing:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Developing
|
||||
|
||||
extra/developing/philosophy.rst
|
||||
extra/developing/api-status.rst
|
||||
extra/developing/test-servers.rst
|
||||
extra/developing/project-structure.rst
|
||||
extra/developing/coding-style.rst
|
||||
extra/developing/understanding-the-type-language.rst
|
||||
extra/developing/tips-for-porting-the-project.rst
|
||||
extra/developing/telegram-api-in-other-languages.rst
|
||||
|
||||
|
||||
.. _More:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: More
|
||||
|
||||
extra/changelog
|
||||
extra/wall-of-shame.rst
|
||||
|
||||
|
||||
.. toctree::
|
||||
:caption: Telethon modules
|
||||
|
||||
telethon
|
||||
|
||||
modules
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
|
|
@ -42,14 +42,6 @@ telethon\.crypto\.factorization module
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.crypto\.libssl module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: telethon.crypto.libssl
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.crypto\.rsa module
|
||||
----------------------------
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
.. _telethon-errors-package:
|
||||
|
||||
|
||||
telethon\.errors package
|
||||
========================
|
||||
|
||||
|
|
9
readthedocs/telethon.events.rst
Normal file
9
readthedocs/telethon.events.rst
Normal file
|
@ -0,0 +1,9 @@
|
|||
.. _telethon-events-package:
|
||||
|
||||
telethon\.events package
|
||||
========================
|
||||
|
||||
.. automodule:: telethon.events
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,11 +1,14 @@
|
|||
.. _telethon-package:
|
||||
|
||||
|
||||
telethon package
|
||||
================
|
||||
|
||||
|
||||
telethon\.helpers module
|
||||
------------------------
|
||||
telethon\.telegram\_client module
|
||||
---------------------------------
|
||||
|
||||
.. automodule:: telethon.helpers
|
||||
.. automodule:: telethon.telegram_client
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
@ -18,14 +21,30 @@ telethon\.telegram\_bare\_client module
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.telegram\_client module
|
||||
---------------------------------
|
||||
telethon\.utils module
|
||||
----------------------
|
||||
|
||||
.. automodule:: telethon.telegram_client
|
||||
.. automodule:: telethon.utils
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.helpers module
|
||||
------------------------
|
||||
|
||||
.. automodule:: telethon.helpers
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.events package
|
||||
------------------------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telethon.events
|
||||
|
||||
|
||||
telethon\.update\_state module
|
||||
------------------------------
|
||||
|
||||
|
@ -34,15 +53,14 @@ telethon\.update\_state module
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.utils module
|
||||
----------------------
|
||||
telethon\.sessions module
|
||||
-------------------------
|
||||
|
||||
.. automodule:: telethon.utils
|
||||
.. automodule:: telethon.sessions
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
telethon\.cryto package
|
||||
------------------------
|
||||
|
||||
|
@ -58,21 +76,21 @@ telethon\.errors package
|
|||
telethon.errors
|
||||
|
||||
telethon\.extensions package
|
||||
------------------------
|
||||
----------------------------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telethon.extensions
|
||||
|
||||
telethon\.network package
|
||||
------------------------
|
||||
-------------------------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telethon.network
|
||||
|
||||
telethon\.tl package
|
||||
------------------------
|
||||
--------------------
|
||||
|
||||
.. toctree::
|
||||
|
||||
|
|
|
@ -10,3 +10,12 @@ telethon\.tl\.custom\.draft module
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
|
||||
telethon\.tl\.custom\.dialog module
|
||||
-----------------------------------
|
||||
|
||||
.. automodule:: telethon.tl.custom.dialog
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
|
@ -7,14 +7,6 @@ telethon\.tl package
|
|||
telethon.tl.custom
|
||||
|
||||
|
||||
telethon\.tl\.entity\_database module
|
||||
-------------------------------------
|
||||
|
||||
.. automodule:: telethon.tl.entity_database
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.tl\.gzip\_packed module
|
||||
---------------------------------
|
||||
|
||||
|
@ -31,14 +23,6 @@ telethon\.tl\.message\_container module
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.tl\.session module
|
||||
----------------------------
|
||||
|
||||
.. automodule:: telethon.tl.session
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.tl\.tl\_message module
|
||||
--------------------------------
|
||||
|
||||
|
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
pyaes
|
||||
rsa
|
||||
typing
|
24
setup.py
24
setup.py
|
@ -13,7 +13,7 @@ Extra supported commands are:
|
|||
|
||||
# To use a consistent encoding
|
||||
from codecs import open
|
||||
from sys import argv
|
||||
from sys import argv, version_info
|
||||
import os
|
||||
import re
|
||||
|
||||
|
@ -45,11 +45,13 @@ GENERATOR_DIR = 'telethon/tl'
|
|||
IMPORT_DEPTH = 2
|
||||
|
||||
|
||||
def gen_tl():
|
||||
def gen_tl(force=True):
|
||||
from telethon_generator.tl_generator import TLGenerator
|
||||
from telethon_generator.error_generator import generate_code
|
||||
generator = TLGenerator(GENERATOR_DIR)
|
||||
if generator.tlobjects_exist():
|
||||
if not force:
|
||||
return
|
||||
print('Detected previous TLObjects. Cleaning...')
|
||||
generator.clean_tlobjects()
|
||||
|
||||
|
@ -99,12 +101,16 @@ def main():
|
|||
fetch_errors(ERRORS_JSON)
|
||||
|
||||
else:
|
||||
# Call gen_tl() if the scheme.tl file exists, e.g. install from GitHub
|
||||
if os.path.isfile(SCHEME_TL):
|
||||
gen_tl(force=False)
|
||||
|
||||
# Get the long description from the README file
|
||||
with open('README.rst', encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
with open('telethon/version.py', encoding='utf-8') as f:
|
||||
version = re.search(r"^__version__\s+=\s+'(.*)'$",
|
||||
version = re.search(r"^__version__\s*=\s*'(.*)'.*$",
|
||||
f.read(), flags=re.MULTILINE).group(1)
|
||||
setup(
|
||||
name='Telethon',
|
||||
|
@ -141,9 +147,17 @@ def main():
|
|||
keywords='telegram api chat client library messaging mtproto',
|
||||
packages=find_packages(exclude=[
|
||||
'telethon_generator', 'telethon_tests', 'run_tests.py',
|
||||
'try_telethon.py'
|
||||
'try_telethon.py',
|
||||
'telethon_generator/parser/__init__.py',
|
||||
'telethon_generator/parser/source_builder.py',
|
||||
'telethon_generator/parser/tl_object.py',
|
||||
'telethon_generator/parser/tl_parser.py',
|
||||
]),
|
||||
install_requires=['pyaes', 'rsa']
|
||||
install_requires=['pyaes', 'rsa',
|
||||
'typing' if version_info < (3, 5, 2) else ""],
|
||||
extras_require={
|
||||
'cryptg': ['cryptg']
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -3,86 +3,90 @@ AES IGE implementation in Python. This module may use libssl if available.
|
|||
"""
|
||||
import os
|
||||
import pyaes
|
||||
from . import libssl
|
||||
|
||||
try:
|
||||
import cryptg
|
||||
except ImportError:
|
||||
cryptg = None
|
||||
|
||||
|
||||
if libssl.AES is not None:
|
||||
# Use libssl if available, since it will be faster
|
||||
AES = libssl.AES
|
||||
else:
|
||||
# Fallback to a pure Python implementation
|
||||
class AES:
|
||||
class AES:
|
||||
"""
|
||||
Class that servers as an interface to encrypt and decrypt
|
||||
text through the AES IGE mode.
|
||||
"""
|
||||
@staticmethod
|
||||
def decrypt_ige(cipher_text, key, iv):
|
||||
"""
|
||||
Class that servers as an interface to encrypt and decrypt
|
||||
text through the AES IGE mode.
|
||||
Decrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
@staticmethod
|
||||
def decrypt_ige(cipher_text, key, iv):
|
||||
"""
|
||||
Decrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
iv1 = iv[:len(iv) // 2]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
if cryptg:
|
||||
return cryptg.decrypt_ige(cipher_text, key, iv)
|
||||
|
||||
aes = pyaes.AES(key)
|
||||
iv1 = iv[:len(iv) // 2]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
|
||||
plain_text = []
|
||||
blocks_count = len(cipher_text) // 16
|
||||
aes = pyaes.AES(key)
|
||||
|
||||
cipher_text_block = [0] * 16
|
||||
for block_index in range(blocks_count):
|
||||
for i in range(16):
|
||||
cipher_text_block[i] = \
|
||||
cipher_text[block_index * 16 + i] ^ iv2[i]
|
||||
plain_text = []
|
||||
blocks_count = len(cipher_text) // 16
|
||||
|
||||
plain_text_block = aes.decrypt(cipher_text_block)
|
||||
cipher_text_block = [0] * 16
|
||||
for block_index in range(blocks_count):
|
||||
for i in range(16):
|
||||
cipher_text_block[i] = \
|
||||
cipher_text[block_index * 16 + i] ^ iv2[i]
|
||||
|
||||
for i in range(16):
|
||||
plain_text_block[i] ^= iv1[i]
|
||||
plain_text_block = aes.decrypt(cipher_text_block)
|
||||
|
||||
iv1 = cipher_text[block_index * 16:block_index * 16 + 16]
|
||||
iv2 = plain_text_block
|
||||
for i in range(16):
|
||||
plain_text_block[i] ^= iv1[i]
|
||||
|
||||
plain_text.extend(plain_text_block)
|
||||
iv1 = cipher_text[block_index * 16:block_index * 16 + 16]
|
||||
iv2 = plain_text_block
|
||||
|
||||
return bytes(plain_text)
|
||||
plain_text.extend(plain_text_block)
|
||||
|
||||
@staticmethod
|
||||
def encrypt_ige(plain_text, key, iv):
|
||||
"""
|
||||
Encrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
return bytes(plain_text)
|
||||
|
||||
# Add random padding iff it's not evenly divisible by 16 already
|
||||
if len(plain_text) % 16 != 0:
|
||||
padding_count = 16 - len(plain_text) % 16
|
||||
plain_text += os.urandom(padding_count)
|
||||
@staticmethod
|
||||
def encrypt_ige(plain_text, key, iv):
|
||||
"""
|
||||
Encrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
# Add random padding iff it's not evenly divisible by 16 already
|
||||
if len(plain_text) % 16 != 0:
|
||||
padding_count = 16 - len(plain_text) % 16
|
||||
plain_text += os.urandom(padding_count)
|
||||
|
||||
iv1 = iv[:len(iv) // 2]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
if cryptg:
|
||||
return cryptg.encrypt_ige(plain_text, key, iv)
|
||||
|
||||
aes = pyaes.AES(key)
|
||||
iv1 = iv[:len(iv) // 2]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
|
||||
cipher_text = []
|
||||
blocks_count = len(plain_text) // 16
|
||||
aes = pyaes.AES(key)
|
||||
|
||||
for block_index in range(blocks_count):
|
||||
plain_text_block = list(
|
||||
plain_text[block_index * 16:block_index * 16 + 16]
|
||||
)
|
||||
for i in range(16):
|
||||
plain_text_block[i] ^= iv1[i]
|
||||
cipher_text = []
|
||||
blocks_count = len(plain_text) // 16
|
||||
|
||||
cipher_text_block = aes.encrypt(plain_text_block)
|
||||
for block_index in range(blocks_count):
|
||||
plain_text_block = list(
|
||||
plain_text[block_index * 16:block_index * 16 + 16]
|
||||
)
|
||||
for i in range(16):
|
||||
plain_text_block[i] ^= iv1[i]
|
||||
|
||||
for i in range(16):
|
||||
cipher_text_block[i] ^= iv2[i]
|
||||
cipher_text_block = aes.encrypt(plain_text_block)
|
||||
|
||||
iv1 = cipher_text_block
|
||||
iv2 = plain_text[block_index * 16:block_index * 16 + 16]
|
||||
for i in range(16):
|
||||
cipher_text_block[i] ^= iv2[i]
|
||||
|
||||
cipher_text.extend(cipher_text_block)
|
||||
iv1 = cipher_text_block
|
||||
iv2 = plain_text[block_index * 16:block_index * 16 + 16]
|
||||
|
||||
return bytes(cipher_text)
|
||||
cipher_text.extend(cipher_text_block)
|
||||
|
||||
return bytes(cipher_text)
|
||||
|
|
|
@ -4,7 +4,6 @@ This module holds the AuthKey class.
|
|||
import struct
|
||||
from hashlib import sha1
|
||||
|
||||
from .. import helpers as utils
|
||||
from ..extensions import BinaryReader
|
||||
|
||||
|
||||
|
@ -36,4 +35,6 @@ class AuthKey:
|
|||
"""
|
||||
new_nonce = new_nonce.to_bytes(32, 'little', signed=True)
|
||||
data = new_nonce + struct.pack('<BQ', number, self.aux_hash)
|
||||
return utils.calc_msg_key(data)
|
||||
|
||||
# Calculates the message key from the given data
|
||||
return sha1(data).digest()[4:20]
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
"""
|
||||
This module holds an AES IGE class, if libssl is available on the system.
|
||||
"""
|
||||
import os
|
||||
import ctypes
|
||||
from ctypes.util import find_library
|
||||
|
||||
lib = find_library('ssl')
|
||||
if not lib:
|
||||
AES = None
|
||||
else:
|
||||
""" <aes.h>
|
||||
# define AES_ENCRYPT 1
|
||||
# define AES_DECRYPT 0
|
||||
# define AES_MAXNR 14
|
||||
struct aes_key_st {
|
||||
# ifdef AES_LONG
|
||||
unsigned long rd_key[4 * (AES_MAXNR + 1)];
|
||||
# else
|
||||
unsigned int rd_key[4 * (AES_MAXNR + 1)];
|
||||
# endif
|
||||
int rounds;
|
||||
};
|
||||
typedef struct aes_key_st AES_KEY;
|
||||
|
||||
int AES_set_encrypt_key(const unsigned char *userKey, const int bits,
|
||||
AES_KEY *key);
|
||||
int AES_set_decrypt_key(const unsigned char *userKey, const int bits,
|
||||
AES_KEY *key);
|
||||
void AES_ige_encrypt(const unsigned char *in, unsigned char *out,
|
||||
size_t length, const AES_KEY *key,
|
||||
unsigned char *ivec, const int enc);
|
||||
"""
|
||||
_libssl = ctypes.cdll.LoadLibrary(lib)
|
||||
|
||||
AES_MAXNR = 14
|
||||
AES_ENCRYPT = ctypes.c_int(1)
|
||||
AES_DECRYPT = ctypes.c_int(0)
|
||||
|
||||
class AES_KEY(ctypes.Structure):
|
||||
"""Helper class representing an AES key"""
|
||||
_fields_ = [
|
||||
('rd_key', ctypes.c_uint32 * (4*(AES_MAXNR + 1))),
|
||||
('rounds', ctypes.c_uint),
|
||||
]
|
||||
|
||||
class AES:
|
||||
"""
|
||||
Class that servers as an interface to encrypt and decrypt
|
||||
text through the AES IGE mode, using the system's libssl.
|
||||
"""
|
||||
@staticmethod
|
||||
def decrypt_ige(cipher_text, key, iv):
|
||||
"""
|
||||
Decrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
aeskey = AES_KEY()
|
||||
ckey = (ctypes.c_ubyte * len(key))(*key)
|
||||
cklen = ctypes.c_int(len(key)*8)
|
||||
cin = (ctypes.c_ubyte * len(cipher_text))(*cipher_text)
|
||||
ctlen = ctypes.c_size_t(len(cipher_text))
|
||||
cout = (ctypes.c_ubyte * len(cipher_text))()
|
||||
civ = (ctypes.c_ubyte * len(iv))(*iv)
|
||||
|
||||
_libssl.AES_set_decrypt_key(ckey, cklen, ctypes.byref(aeskey))
|
||||
_libssl.AES_ige_encrypt(
|
||||
ctypes.byref(cin),
|
||||
ctypes.byref(cout),
|
||||
ctlen,
|
||||
ctypes.byref(aeskey),
|
||||
ctypes.byref(civ),
|
||||
AES_DECRYPT
|
||||
)
|
||||
|
||||
return bytes(cout)
|
||||
|
||||
@staticmethod
|
||||
def encrypt_ige(plain_text, key, iv):
|
||||
"""
|
||||
Encrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
# Add random padding iff it's not evenly divisible by 16 already
|
||||
if len(plain_text) % 16 != 0:
|
||||
padding_count = 16 - len(plain_text) % 16
|
||||
plain_text += os.urandom(padding_count)
|
||||
|
||||
aeskey = AES_KEY()
|
||||
ckey = (ctypes.c_ubyte * len(key))(*key)
|
||||
cklen = ctypes.c_int(len(key)*8)
|
||||
cin = (ctypes.c_ubyte * len(plain_text))(*plain_text)
|
||||
ctlen = ctypes.c_size_t(len(plain_text))
|
||||
cout = (ctypes.c_ubyte * len(plain_text))()
|
||||
civ = (ctypes.c_ubyte * len(iv))(*iv)
|
||||
|
||||
_libssl.AES_set_encrypt_key(ckey, cklen, ctypes.byref(aeskey))
|
||||
_libssl.AES_ige_encrypt(
|
||||
ctypes.byref(cin),
|
||||
ctypes.byref(cout),
|
||||
ctlen,
|
||||
ctypes.byref(aeskey),
|
||||
ctypes.byref(civ),
|
||||
AES_ENCRYPT
|
||||
)
|
||||
|
||||
return bytes(cout)
|
|
@ -7,9 +7,8 @@ import re
|
|||
from threading import Thread
|
||||
|
||||
from .common import (
|
||||
ReadCancelledError, InvalidParameterError, TypeNotFoundError,
|
||||
InvalidChecksumError, BrokenAuthKeyError, SecurityError,
|
||||
CdnFileTamperedError
|
||||
ReadCancelledError, TypeNotFoundError, InvalidChecksumError,
|
||||
BrokenAuthKeyError, SecurityError, CdnFileTamperedError
|
||||
)
|
||||
|
||||
# This imports the base errors too, as they're imported there
|
||||
|
@ -79,6 +78,9 @@ def rpc_message_to_error(code, message, report_method=None):
|
|||
if code == 404:
|
||||
return NotFoundError(message)
|
||||
|
||||
if code == 406:
|
||||
return AuthKeyError(message)
|
||||
|
||||
if code == 500:
|
||||
return ServerError(message)
|
||||
|
||||
|
|
|
@ -4,14 +4,7 @@
|
|||
class ReadCancelledError(Exception):
|
||||
"""Occurs when a read operation was cancelled."""
|
||||
def __init__(self):
|
||||
super().__init__(self, 'The read operation was cancelled.')
|
||||
|
||||
|
||||
class InvalidParameterError(Exception):
|
||||
"""
|
||||
Occurs when an invalid parameter is given, for example,
|
||||
when either A or B are required but none is given.
|
||||
"""
|
||||
super().__init__('The read operation was cancelled.')
|
||||
|
||||
|
||||
class TypeNotFoundError(Exception):
|
||||
|
@ -21,7 +14,7 @@ class TypeNotFoundError(Exception):
|
|||
"""
|
||||
def __init__(self, invalid_constructor_id):
|
||||
super().__init__(
|
||||
self, 'Could not find a matching Constructor ID for the TLObject '
|
||||
'Could not find a matching Constructor ID for the TLObject '
|
||||
'that was supposed to be read with ID {}. Most likely, a TLObject '
|
||||
'was trying to be read when it should not be read.'
|
||||
.format(hex(invalid_constructor_id)))
|
||||
|
@ -36,7 +29,6 @@ class InvalidChecksumError(Exception):
|
|||
"""
|
||||
def __init__(self, checksum, valid_checksum):
|
||||
super().__init__(
|
||||
self,
|
||||
'Invalid checksum ({} when {} was expected). '
|
||||
'This packet should be skipped.'
|
||||
.format(checksum, valid_checksum))
|
||||
|
@ -51,7 +43,6 @@ class BrokenAuthKeyError(Exception):
|
|||
"""
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
self,
|
||||
'The authorization key is broken, and it must be reset.'
|
||||
)
|
||||
|
||||
|
@ -63,7 +54,7 @@ class SecurityError(Exception):
|
|||
def __init__(self, *args):
|
||||
if not args:
|
||||
args = ['A security check failed.']
|
||||
super().__init__(self, *args)
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
class CdnFileTamperedError(SecurityError):
|
||||
|
|
|
@ -40,7 +40,7 @@ class ForbiddenError(RPCError):
|
|||
message = 'FORBIDDEN'
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__(self, message)
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
|
@ -52,7 +52,20 @@ class NotFoundError(RPCError):
|
|||
message = 'NOT_FOUND'
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__(self, message)
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class AuthKeyError(RPCError):
|
||||
"""
|
||||
Errors related to invalid authorization key, like
|
||||
AUTH_KEY_DUPLICATED which can cause the connection to fail.
|
||||
"""
|
||||
code = 406
|
||||
message = 'AUTH_KEY'
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
|
@ -77,7 +90,7 @@ class ServerError(RPCError):
|
|||
message = 'INTERNAL'
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__(self, message)
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
|
@ -121,7 +134,7 @@ class BadMessageError(Exception):
|
|||
}
|
||||
|
||||
def __init__(self, code):
|
||||
super().__init__(self, self.ErrorMessages.get(
|
||||
super().__init__(self.ErrorMessages.get(
|
||||
code,
|
||||
'Unknown error code (this should not happen): {}.'.format(code)))
|
||||
|
||||
|
|
1288
telethon/events/__init__.py
Normal file
1288
telethon/events/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -6,7 +6,7 @@ from datetime import datetime
|
|||
from io import BufferedReader, BytesIO
|
||||
from struct import unpack
|
||||
|
||||
from ..errors import InvalidParameterError, TypeNotFoundError
|
||||
from ..errors import TypeNotFoundError
|
||||
from ..tl.all_tlobjects import tlobjects
|
||||
|
||||
|
||||
|
@ -22,8 +22,7 @@ class BinaryReader:
|
|||
elif stream:
|
||||
self.stream = stream
|
||||
else:
|
||||
raise InvalidParameterError(
|
||||
'Either bytes or a stream must be provided')
|
||||
raise ValueError('Either bytes or a stream must be provided')
|
||||
|
||||
self.reader = BufferedReader(self.stream)
|
||||
self._last = None # Should come in handy to spot -404 errors
|
||||
|
@ -57,8 +56,11 @@ class BinaryReader:
|
|||
return int.from_bytes(
|
||||
self.read(bits // 8), byteorder='little', signed=signed)
|
||||
|
||||
def read(self, length):
|
||||
def read(self, length=None):
|
||||
"""Read the given amount of bytes."""
|
||||
if length is None:
|
||||
return self.reader.read()
|
||||
|
||||
result = self.reader.read(length)
|
||||
if len(result) != length:
|
||||
raise BufferError(
|
||||
|
@ -110,7 +112,7 @@ class BinaryReader:
|
|||
elif value == 0xbc799737: # boolFalse
|
||||
return False
|
||||
else:
|
||||
raise ValueError('Invalid boolean code {}'.format(hex(value)))
|
||||
raise RuntimeError('Invalid boolean code {}'.format(hex(value)))
|
||||
|
||||
def tgread_date(self):
|
||||
"""Reads and converts Unix time (used by Telegram)
|
||||
|
@ -131,6 +133,8 @@ class BinaryReader:
|
|||
return True
|
||||
elif value == 0xbc799737: # boolFalse
|
||||
return False
|
||||
elif value == 0x1cb5c415: # Vector
|
||||
return [self.tgread_object() for _ in range(self.read_int())]
|
||||
|
||||
# If there was still no luck, give up
|
||||
self.seek(-4) # Go back
|
||||
|
@ -141,7 +145,7 @@ class BinaryReader:
|
|||
def tgread_vector(self):
|
||||
"""Reads a vector (a list) of Telegram objects."""
|
||||
if 0x1cb5c415 != self.read_int(signed=False):
|
||||
raise ValueError('Invalid constructor code, vector was expected')
|
||||
raise RuntimeError('Invalid constructor code, vector was expected')
|
||||
|
||||
count = self.read_int()
|
||||
return [self.tgread_object() for _ in range(count)]
|
||||
|
|
182
telethon/extensions/html.py
Normal file
182
telethon/extensions/html.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
"""
|
||||
Simple HTML -> Telegram entity parser.
|
||||
"""
|
||||
import struct
|
||||
from collections import deque
|
||||
from html import escape, unescape
|
||||
from html.parser import HTMLParser
|
||||
|
||||
from ..tl.types import (
|
||||
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
|
||||
MessageEntityPre, MessageEntityEmail, MessageEntityUrl,
|
||||
MessageEntityTextUrl
|
||||
)
|
||||
|
||||
|
||||
# Helpers from markdown.py
|
||||
def _add_surrogate(text):
|
||||
return ''.join(
|
||||
''.join(chr(y) for y in struct.unpack('<HH', x.encode('utf-16le')))
|
||||
if (0x10000 <= ord(x) <= 0x10FFFF) else x for x in text
|
||||
)
|
||||
|
||||
|
||||
def _del_surrogate(text):
|
||||
return text.encode('utf-16', 'surrogatepass').decode('utf-16')
|
||||
|
||||
|
||||
class HTMLToTelegramParser(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.text = ''
|
||||
self.entities = []
|
||||
self._building_entities = {}
|
||||
self._open_tags = deque()
|
||||
self._open_tags_meta = deque()
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
self._open_tags.appendleft(tag)
|
||||
self._open_tags_meta.appendleft(None)
|
||||
|
||||
attrs = dict(attrs)
|
||||
EntityType = None
|
||||
args = {}
|
||||
if tag == 'strong' or tag == 'b':
|
||||
EntityType = MessageEntityBold
|
||||
elif tag == 'em' or tag == 'i':
|
||||
EntityType = MessageEntityItalic
|
||||
elif tag == 'code':
|
||||
try:
|
||||
# If we're in the middle of a <pre> tag, this <code> tag is
|
||||
# probably intended for syntax highlighting.
|
||||
#
|
||||
# Syntax highlighting is set with
|
||||
# <code class='language-...'>codeblock</code>
|
||||
# inside <pre> tags
|
||||
pre = self._building_entities['pre']
|
||||
try:
|
||||
pre.language = attrs['class'][len('language-'):]
|
||||
except KeyError:
|
||||
pass
|
||||
except KeyError:
|
||||
EntityType = MessageEntityCode
|
||||
elif tag == 'pre':
|
||||
EntityType = MessageEntityPre
|
||||
args['language'] = ''
|
||||
elif tag == 'a':
|
||||
try:
|
||||
url = attrs['href']
|
||||
except KeyError:
|
||||
return
|
||||
if url.startswith('mailto:'):
|
||||
url = url[len('mailto:'):]
|
||||
EntityType = MessageEntityEmail
|
||||
else:
|
||||
if self.get_starttag_text() == url:
|
||||
EntityType = MessageEntityUrl
|
||||
else:
|
||||
EntityType = MessageEntityTextUrl
|
||||
args['url'] = url
|
||||
url = None
|
||||
self._open_tags_meta.popleft()
|
||||
self._open_tags_meta.appendleft(url)
|
||||
|
||||
if EntityType and tag not in self._building_entities:
|
||||
self._building_entities[tag] = EntityType(
|
||||
offset=len(self.text),
|
||||
# The length will be determined when closing the tag.
|
||||
length=0,
|
||||
**args)
|
||||
|
||||
def handle_data(self, text):
|
||||
text = unescape(text)
|
||||
|
||||
previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
|
||||
if previous_tag == 'a':
|
||||
url = self._open_tags_meta[0]
|
||||
if url:
|
||||
text = url
|
||||
|
||||
for tag, entity in self._building_entities.items():
|
||||
entity.length += len(text.strip('\n'))
|
||||
|
||||
self.text += text
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
try:
|
||||
self._open_tags.popleft()
|
||||
self._open_tags_meta.popleft()
|
||||
except IndexError:
|
||||
pass
|
||||
entity = self._building_entities.pop(tag, None)
|
||||
if entity:
|
||||
self.entities.append(entity)
|
||||
|
||||
|
||||
def parse(html):
|
||||
"""
|
||||
Parses the given HTML message and returns its stripped representation
|
||||
plus a list of the MessageEntity's that were found.
|
||||
|
||||
:param message: the message with HTML to be parsed.
|
||||
:return: a tuple consisting of (clean message, [message entities]).
|
||||
"""
|
||||
parser = HTMLToTelegramParser()
|
||||
parser.feed(_add_surrogate(html))
|
||||
return _del_surrogate(parser.text), parser.entities
|
||||
|
||||
|
||||
def unparse(text, entities):
|
||||
"""
|
||||
Performs the reverse operation to .parse(), effectively returning HTML
|
||||
given a normal text and its MessageEntity's.
|
||||
|
||||
:param text: the text to be reconverted into HTML.
|
||||
:param entities: the MessageEntity's applied to the text.
|
||||
:return: a HTML representation of the combination of both inputs.
|
||||
"""
|
||||
if not entities:
|
||||
return text
|
||||
|
||||
text = _add_surrogate(text)
|
||||
html = []
|
||||
last_offset = 0
|
||||
for entity in entities:
|
||||
if entity.offset > last_offset:
|
||||
html.append(escape(text[last_offset:entity.offset]))
|
||||
elif entity.offset < last_offset:
|
||||
continue
|
||||
|
||||
skip_entity = False
|
||||
entity_text = escape(text[entity.offset:entity.offset + entity.length])
|
||||
entity_type = type(entity)
|
||||
|
||||
if entity_type == MessageEntityBold:
|
||||
html.append('<strong>{}</strong>'.format(entity_text))
|
||||
elif entity_type == MessageEntityItalic:
|
||||
html.append('<em>{}</em>'.format(entity_text))
|
||||
elif entity_type == MessageEntityCode:
|
||||
html.append('<code>{}</code>'.format(entity_text))
|
||||
elif entity_type == MessageEntityPre:
|
||||
if entity.language:
|
||||
html.append(
|
||||
"<pre>\n"
|
||||
" <code class='language-{}'>\n"
|
||||
" {}\n"
|
||||
" </code>\n"
|
||||
"</pre>".format(entity.language, entity_text))
|
||||
else:
|
||||
html.append('<pre><code>{}</code></pre>'
|
||||
.format(entity_text))
|
||||
elif entity_type == MessageEntityEmail:
|
||||
html.append('<a href="mailto:{0}">{0}</a>'.format(entity_text))
|
||||
elif entity_type == MessageEntityUrl:
|
||||
html.append('<a href="{0}">{0}</a>'.format(entity_text))
|
||||
elif entity_type == MessageEntityTextUrl:
|
||||
html.append('<a href="{}">{}</a>'
|
||||
.format(escape(entity.url), entity_text))
|
||||
else:
|
||||
skip_entity = True
|
||||
last_offset = entity.offset + (0 if skip_entity else entity.length)
|
||||
html.append(text[last_offset:])
|
||||
return _del_surrogate(''.join(html))
|
|
@ -4,6 +4,7 @@ for use within the library, which attempts to handle emojies correctly,
|
|||
since they seem to count as two characters and it's a bit strange.
|
||||
"""
|
||||
import re
|
||||
import struct
|
||||
|
||||
from ..tl import TLObject
|
||||
|
||||
|
@ -20,15 +21,21 @@ DEFAULT_DELIMITERS = {
|
|||
'```': MessageEntityPre
|
||||
}
|
||||
|
||||
# Regex used to match utf-16le encoded r'\[(.+?)\]\((.+?)\)',
|
||||
# reason why there's '\0' after every match-literal character.
|
||||
DEFAULT_URL_RE = re.compile(b'\\[\0(.+?)\\]\0\\(\0(.+?)\\)\0')
|
||||
|
||||
# Reverse operation for DEFAULT_URL_RE. {0} for text, {1} for URL.
|
||||
DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)')
|
||||
DEFAULT_URL_FORMAT = '[{0}]({1})'
|
||||
|
||||
# Encoding to be used
|
||||
ENC = 'utf-16le'
|
||||
|
||||
def _add_surrogate(text):
|
||||
return ''.join(
|
||||
# SMP -> Surrogate Pairs (Telegram offsets are calculated with these).
|
||||
# See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more.
|
||||
''.join(chr(y) for y in struct.unpack('<HH', x.encode('utf-16le')))
|
||||
if (0x10000 <= ord(x) <= 0x10FFFF) else x for x in text
|
||||
)
|
||||
|
||||
|
||||
def _del_surrogate(text):
|
||||
return text.encode('utf-16', 'surrogatepass').decode('utf-16')
|
||||
|
||||
|
||||
def parse(message, delimiters=None, url_re=None):
|
||||
|
@ -43,17 +50,14 @@ def parse(message, delimiters=None, url_re=None):
|
|||
"""
|
||||
if url_re is None:
|
||||
url_re = DEFAULT_URL_RE
|
||||
elif url_re:
|
||||
if isinstance(url_re, bytes):
|
||||
url_re = re.compile(url_re)
|
||||
elif isinstance(url_re, str):
|
||||
url_re = re.compile(url_re)
|
||||
|
||||
if not delimiters:
|
||||
if delimiters is not None:
|
||||
return message, []
|
||||
delimiters = DEFAULT_DELIMITERS
|
||||
|
||||
delimiters = {k.encode(ENC): v for k, v in delimiters.items()}
|
||||
|
||||
# Cannot use a for loop because we need to skip some indices
|
||||
i = 0
|
||||
result = []
|
||||
|
@ -62,7 +66,7 @@ def parse(message, delimiters=None, url_re=None):
|
|||
|
||||
# Work on byte level with the utf-16le encoding to get the offsets right.
|
||||
# The offset will just be half the index we're at.
|
||||
message = message.encode(ENC)
|
||||
message = _add_surrogate(message)
|
||||
while i < len(message):
|
||||
if url_re and current is None:
|
||||
# If we're not inside a previous match since Telegram doesn't allow
|
||||
|
@ -70,15 +74,15 @@ def parse(message, delimiters=None, url_re=None):
|
|||
url_match = url_re.match(message, pos=i)
|
||||
if url_match:
|
||||
# Replace the whole match with only the inline URL text.
|
||||
message = b''.join((
|
||||
message = ''.join((
|
||||
message[:url_match.start()],
|
||||
url_match.group(1),
|
||||
message[url_match.end():]
|
||||
))
|
||||
|
||||
result.append(MessageEntityTextUrl(
|
||||
offset=i // 2, length=len(url_match.group(1)) // 2,
|
||||
url=url_match.group(2).decode(ENC)
|
||||
offset=i, length=len(url_match.group(1)),
|
||||
url=_del_surrogate(url_match.group(2))
|
||||
))
|
||||
i += len(url_match.group(1))
|
||||
# Next loop iteration, don't check delimiters, since
|
||||
|
@ -103,16 +107,16 @@ def parse(message, delimiters=None, url_re=None):
|
|||
message = message[:i] + message[i + len(d):]
|
||||
if m == MessageEntityPre:
|
||||
# Special case, also has 'lang'
|
||||
current = m(i // 2, None, '')
|
||||
current = m(i, None, '')
|
||||
else:
|
||||
current = m(i // 2, None)
|
||||
current = m(i, None)
|
||||
|
||||
end_delimiter = d # We expect the same delimiter.
|
||||
break
|
||||
|
||||
elif message[i:i + len(end_delimiter)] == end_delimiter:
|
||||
message = message[:i] + message[i + len(end_delimiter):]
|
||||
current.length = (i // 2) - current.offset
|
||||
current.length = i - current.offset
|
||||
result.append(current)
|
||||
current, end_delimiter = None, None
|
||||
# Don't increment i here as we matched a delimiter,
|
||||
|
@ -121,19 +125,19 @@ def parse(message, delimiters=None, url_re=None):
|
|||
# as we already know there won't be the same right after.
|
||||
continue
|
||||
|
||||
# Next iteration, utf-16 encoded characters need 2 bytes.
|
||||
i += 2
|
||||
# Next iteration
|
||||
i += 1
|
||||
|
||||
# We may have found some a delimiter but not its ending pair.
|
||||
# If this is the case, we want to insert the delimiter character back.
|
||||
if current is not None:
|
||||
message = (
|
||||
message[:2 * current.offset]
|
||||
message[:current.offset]
|
||||
+ end_delimiter
|
||||
+ message[2 * current.offset:]
|
||||
+ message[current.offset:]
|
||||
)
|
||||
|
||||
return message.decode(ENC), result
|
||||
return _del_surrogate(message), result
|
||||
|
||||
|
||||
def unparse(text, entities, delimiters=None, url_fmt=None):
|
||||
|
@ -145,6 +149,9 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
|
|||
:param entities: the MessageEntity's applied to the text.
|
||||
:return: a markdown-like text representing the combination of both inputs.
|
||||
"""
|
||||
if not entities:
|
||||
return text
|
||||
|
||||
if not delimiters:
|
||||
if delimiters is not None:
|
||||
return text
|
||||
|
@ -158,29 +165,22 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
|
|||
else:
|
||||
entities = tuple(sorted(entities, key=lambda e: e.offset, reverse=True))
|
||||
|
||||
# Reverse the delimiters, and encode them as utf16
|
||||
delimiters = {v: k.encode(ENC) for k, v in delimiters.items()}
|
||||
text = text.encode(ENC)
|
||||
text = _add_surrogate(text)
|
||||
delimiters = {v: k for k, v in delimiters.items()}
|
||||
for entity in entities:
|
||||
s = entity.offset * 2
|
||||
e = (entity.offset + entity.length) * 2
|
||||
s = entity.offset
|
||||
e = entity.offset + entity.length
|
||||
delimiter = delimiters.get(type(entity), None)
|
||||
if delimiter:
|
||||
text = text[:s] + delimiter + text[s:e] + delimiter + text[e:]
|
||||
elif isinstance(entity, MessageEntityTextUrl) and url_fmt:
|
||||
# If byte-strings supported .format(), we, could have converted
|
||||
# the str url_fmt to a byte-string with the following regex:
|
||||
# re.sub(b'{\0\s*(?:([01])\0)?\s*}\0',rb'{\1}',url_fmt.encode(ENC))
|
||||
#
|
||||
# This would preserve {}, {0} and {1}.
|
||||
# Alternatively (as it's done), we can decode/encode it every time.
|
||||
text = (
|
||||
text[:s] +
|
||||
url_fmt.format(text[s:e].decode(ENC), entity.url).encode(ENC) +
|
||||
_add_surrogate(url_fmt.format(text[s:e], entity.url)) +
|
||||
text[e:]
|
||||
)
|
||||
|
||||
return text.decode(ENC)
|
||||
return _del_surrogate(text)
|
||||
|
||||
|
||||
def get_inner_text(text, entity):
|
||||
|
@ -192,17 +192,17 @@ def get_inner_text(text, entity):
|
|||
:param entity: the entity or entities that must be matched.
|
||||
:return: a single result or a list of the text surrounded by the entities.
|
||||
"""
|
||||
if not isinstance(entity, TLObject) and hasattr(entity, '__iter__'):
|
||||
if isinstance(entity, TLObject):
|
||||
entity = (entity,)
|
||||
multiple = True
|
||||
else:
|
||||
entity = [entity]
|
||||
multiple = False
|
||||
|
||||
text = text.encode(ENC)
|
||||
text = _add_surrogate(text)
|
||||
result = []
|
||||
for e in entity:
|
||||
start = e.offset * 2
|
||||
end = (e.offset + e.length) * 2
|
||||
result.append(text[start:end].decode(ENC))
|
||||
start = e.offset
|
||||
end = e.offset + e.length
|
||||
result.append(_del_surrogate(text[start:end]))
|
||||
|
||||
return result if multiple else result[0]
|
||||
|
|
|
@ -2,11 +2,26 @@
|
|||
This module holds a rough implementation of the C# TCP client.
|
||||
"""
|
||||
import errno
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
from datetime import timedelta
|
||||
from io import BytesIO, BufferedWriter
|
||||
from threading import Lock
|
||||
|
||||
try:
|
||||
import socks
|
||||
except ImportError:
|
||||
socks = None
|
||||
|
||||
MAX_TIMEOUT = 15 # in seconds
|
||||
CONN_RESET_ERRNOS = {
|
||||
errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH,
|
||||
errno.EINVAL, errno.ENOTCONN
|
||||
}
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TcpClient:
|
||||
"""A simple TCP client to ease the work with sockets and proxies."""
|
||||
|
@ -26,7 +41,7 @@ class TcpClient:
|
|||
elif isinstance(timeout, (int, float)):
|
||||
self.timeout = float(timeout)
|
||||
else:
|
||||
raise ValueError('Invalid timeout type', type(timeout))
|
||||
raise TypeError('Invalid timeout type: {}'.format(type(timeout)))
|
||||
|
||||
def _recreate_socket(self, mode):
|
||||
if self.proxy is None:
|
||||
|
@ -49,16 +64,12 @@ class TcpClient:
|
|||
:param port: the port to connect to.
|
||||
"""
|
||||
if ':' in ip: # IPv6
|
||||
# The address needs to be surrounded by [] as discussed on PR#425
|
||||
if not ip.startswith('['):
|
||||
ip = '[' + ip
|
||||
if not ip.endswith(']'):
|
||||
ip = ip + ']'
|
||||
|
||||
ip = ip.replace('[', '').replace(']', '')
|
||||
mode, address = socket.AF_INET6, (ip, port, 0, 0)
|
||||
else:
|
||||
mode, address = socket.AF_INET, (ip, port)
|
||||
|
||||
timeout = 1
|
||||
while True:
|
||||
try:
|
||||
while not self._socket:
|
||||
|
@ -67,12 +78,20 @@ class TcpClient:
|
|||
self._socket.connect(address)
|
||||
break # Successful connection, stop retrying to connect
|
||||
except OSError as e:
|
||||
__log__.info('OSError "%s" raised while connecting', e)
|
||||
# Stop retrying to connect if proxy connection error occurred
|
||||
if socks and isinstance(e, socks.ProxyConnectionError):
|
||||
raise
|
||||
# There are some errors that we know how to handle, and
|
||||
# the loop will allow us to retry
|
||||
if e.errno == errno.EBADF:
|
||||
if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL,
|
||||
errno.ECONNREFUSED, # Windows-specific follow
|
||||
getattr(errno, 'WSAEACCES', None)):
|
||||
# Bad file descriptor, i.e. socket was closed, set it
|
||||
# to none to recreate it on the next iteration
|
||||
self._socket = None
|
||||
time.sleep(timeout)
|
||||
timeout = min(timeout * 2, MAX_TIMEOUT)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
@ -105,19 +124,22 @@ class TcpClient:
|
|||
:param data: the data to send.
|
||||
"""
|
||||
if self._socket is None:
|
||||
raise ConnectionResetError()
|
||||
self._raise_connection_reset(None)
|
||||
|
||||
# TODO Timeout may be an issue when sending the data, Changed in v3.5:
|
||||
# The socket timeout is now the maximum total duration to send all data.
|
||||
try:
|
||||
self._socket.sendall(data)
|
||||
except socket.timeout as e:
|
||||
__log__.debug('socket.timeout "%s" while writing data', e)
|
||||
raise TimeoutError() from e
|
||||
except ConnectionError:
|
||||
self._raise_connection_reset()
|
||||
except ConnectionError as e:
|
||||
__log__.info('ConnectionError "%s" while writing data', e)
|
||||
self._raise_connection_reset(e)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EBADF:
|
||||
self._raise_connection_reset()
|
||||
__log__.info('OSError "%s" while writing data', e)
|
||||
if e.errno in CONN_RESET_ERRNOS:
|
||||
self._raise_connection_reset(e)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
@ -129,26 +151,42 @@ class TcpClient:
|
|||
:return: the read data with len(data) == size.
|
||||
"""
|
||||
if self._socket is None:
|
||||
raise ConnectionResetError()
|
||||
self._raise_connection_reset(None)
|
||||
|
||||
# TODO Remove the timeout from this method, always use previous one
|
||||
with BufferedWriter(BytesIO(), buffer_size=size) as buffer:
|
||||
bytes_left = size
|
||||
while bytes_left != 0:
|
||||
try:
|
||||
partial = self._socket.recv(bytes_left)
|
||||
except socket.timeout as e:
|
||||
# These are somewhat common if the server has nothing
|
||||
# to send to us, so use a lower logging priority.
|
||||
if bytes_left < size:
|
||||
__log__.warning(
|
||||
'socket.timeout "%s" when %d/%d had been received',
|
||||
e, size - bytes_left, size
|
||||
)
|
||||
else:
|
||||
__log__.debug(
|
||||
'socket.timeout "%s" while reading data', e
|
||||
)
|
||||
|
||||
raise TimeoutError() from e
|
||||
except ConnectionError:
|
||||
self._raise_connection_reset()
|
||||
except ConnectionError as e:
|
||||
__log__.info('ConnectionError "%s" while reading data', e)
|
||||
self._raise_connection_reset(e)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EBADF or e.errno == errno.ENOTSOCK:
|
||||
self._raise_connection_reset()
|
||||
if e.errno != errno.EBADF and self._closing_lock.locked():
|
||||
# Ignore bad file descriptor while closing
|
||||
__log__.info('OSError "%s" while reading data', e)
|
||||
|
||||
if e.errno in CONN_RESET_ERRNOS:
|
||||
self._raise_connection_reset(e)
|
||||
else:
|
||||
raise
|
||||
|
||||
if len(partial) == 0:
|
||||
self._raise_connection_reset()
|
||||
self._raise_connection_reset(None)
|
||||
|
||||
buffer.write(partial)
|
||||
bytes_left -= len(partial)
|
||||
|
@ -157,7 +195,8 @@ class TcpClient:
|
|||
buffer.flush()
|
||||
return buffer.raw.getvalue()
|
||||
|
||||
def _raise_connection_reset(self):
|
||||
def _raise_connection_reset(self, original):
|
||||
"""Disconnects the client and raises ConnectionResetError."""
|
||||
self.close() # Connection reset -> flag as socket closed
|
||||
raise ConnectionResetError('The server has closed the connection.')
|
||||
raise ConnectionResetError('The server has closed the connection.')\
|
||||
from original
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
"""Various helpers not related to the Telegram API itself"""
|
||||
from hashlib import sha1, sha256
|
||||
import os
|
||||
import struct
|
||||
from hashlib import sha1, sha256
|
||||
|
||||
from telethon.crypto import AES
|
||||
from telethon.errors import SecurityError
|
||||
from telethon.extensions import BinaryReader
|
||||
|
||||
|
||||
# region Multiple utilities
|
||||
|
||||
|
@ -21,28 +27,68 @@ def ensure_parent_dir_exists(file_path):
|
|||
# region Cryptographic related utils
|
||||
|
||||
|
||||
def calc_key(shared_key, msg_key, client):
|
||||
"""Calculate the key based on Telegram guidelines,
|
||||
specifying whether it's the client or not
|
||||
def pack_message(session, message):
|
||||
"""Packs a message following MtProto 2.0 guidelines"""
|
||||
# See https://core.telegram.org/mtproto/description
|
||||
data = struct.pack('<qq', session.salt, session.id) + bytes(message)
|
||||
padding = os.urandom(-(len(data) + 12) % 16 + 12)
|
||||
|
||||
# Being substr(what, offset, length); x = 0 for client
|
||||
# "msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)"
|
||||
msg_key_large = sha256(
|
||||
session.auth_key.key[88:88 + 32] + data + padding).digest()
|
||||
|
||||
# "msg_key = substr (msg_key_large, 8, 16)"
|
||||
msg_key = msg_key_large[8:24]
|
||||
aes_key, aes_iv = calc_key(session.auth_key.key, msg_key, True)
|
||||
|
||||
key_id = struct.pack('<Q', session.auth_key.key_id)
|
||||
return key_id + msg_key + AES.encrypt_ige(data + padding, aes_key, aes_iv)
|
||||
|
||||
|
||||
def unpack_message(session, reader):
|
||||
"""Unpacks a message following MtProto 2.0 guidelines"""
|
||||
# See https://core.telegram.org/mtproto/description
|
||||
if reader.read_long(signed=False) != session.auth_key.key_id:
|
||||
raise SecurityError('Server replied with an invalid auth key')
|
||||
|
||||
msg_key = reader.read(16)
|
||||
aes_key, aes_iv = calc_key(session.auth_key.key, msg_key, False)
|
||||
data = BinaryReader(AES.decrypt_ige(reader.read(), aes_key, aes_iv))
|
||||
|
||||
data.read_long() # remote_salt
|
||||
if data.read_long() != session.id:
|
||||
raise SecurityError('Server replied with a wrong session ID')
|
||||
|
||||
remote_msg_id = data.read_long()
|
||||
remote_sequence = data.read_int()
|
||||
msg_len = data.read_int()
|
||||
message = data.read(msg_len)
|
||||
|
||||
# https://core.telegram.org/mtproto/security_guidelines
|
||||
# Sections "checking sha256 hash" and "message length"
|
||||
if msg_key != sha256(
|
||||
session.auth_key.key[96:96 + 32] + data.get_bytes()).digest()[8:24]:
|
||||
raise SecurityError("Received msg_key doesn't match with expected one")
|
||||
|
||||
return message, remote_msg_id, remote_sequence
|
||||
|
||||
|
||||
def calc_key(auth_key, msg_key, client):
|
||||
"""
|
||||
Calculate the key based on Telegram guidelines
|
||||
for MtProto 2, specifying whether it's the client or not.
|
||||
"""
|
||||
# https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector
|
||||
x = 0 if client else 8
|
||||
|
||||
sha1a = sha1(msg_key + shared_key[x:x + 32]).digest()
|
||||
sha1b = sha1(shared_key[x + 32:x + 48] + msg_key +
|
||||
shared_key[x + 48:x + 64]).digest()
|
||||
sha256a = sha256(msg_key + auth_key[x: x + 36]).digest()
|
||||
sha256b = sha256(auth_key[x + 40:x + 76] + msg_key).digest()
|
||||
|
||||
sha1c = sha1(shared_key[x + 64:x + 96] + msg_key).digest()
|
||||
sha1d = sha1(msg_key + shared_key[x + 96:x + 128]).digest()
|
||||
aes_key = sha256a[:8] + sha256b[8:24] + sha256a[24:32]
|
||||
aes_iv = sha256b[:8] + sha256a[8:24] + sha256b[24:32]
|
||||
|
||||
key = sha1a[0:8] + sha1b[8:20] + sha1c[4:16]
|
||||
iv = sha1a[8:20] + sha1b[0:8] + sha1c[16:20] + sha1d[0:8]
|
||||
|
||||
return key, iv
|
||||
|
||||
|
||||
def calc_msg_key(data):
|
||||
"""Calculates the message key from the given data"""
|
||||
return sha1(data).digest()[4:20]
|
||||
return aes_key, aes_iv
|
||||
|
||||
|
||||
def generate_key_data_from_nonce(server_nonce, new_nonce):
|
||||
|
|
|
@ -17,7 +17,7 @@ from ..errors import SecurityError
|
|||
from ..extensions import BinaryReader
|
||||
from ..network import MtProtoPlainSender
|
||||
from ..tl.functions import (
|
||||
ReqPqRequest, ReqDHParamsRequest, SetClientDHParamsRequest
|
||||
ReqPqMultiRequest, ReqDHParamsRequest, SetClientDHParamsRequest
|
||||
)
|
||||
|
||||
|
||||
|
@ -53,7 +53,7 @@ def _do_authentication(connection):
|
|||
sender = MtProtoPlainSender(connection)
|
||||
|
||||
# Step 1 sending: PQ Request, endianness doesn't matter since it's random
|
||||
req_pq_request = ReqPqRequest(
|
||||
req_pq_request = ReqPqMultiRequest(
|
||||
nonce=int.from_bytes(os.urandom(16), 'big', signed=True)
|
||||
)
|
||||
sender.send(bytes(req_pq_request))
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
This module holds both the Connection class and the ConnectionMode enum,
|
||||
which specifies the protocol to be used by the Connection.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from datetime import timedelta
|
||||
|
@ -14,6 +15,8 @@ from ..crypto import AESModeCTR
|
|||
from ..extensions import TcpClient
|
||||
from ..errors import InvalidChecksumError
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConnectionMode(Enum):
|
||||
"""Represents which mode should be used to stabilise a connection.
|
||||
|
@ -181,6 +184,21 @@ class Connection:
|
|||
packet_len_seq = self.read(8) # 4 and 4
|
||||
packet_len, seq = struct.unpack('<ii', packet_len_seq)
|
||||
|
||||
# Sometimes Telegram seems to send a packet length of 0 (12)
|
||||
# and after that, just a single byte. Not sure what this is.
|
||||
# TODO Figure out what this is, and if there's a better fix.
|
||||
if packet_len <= 12:
|
||||
__log__.error('Read invalid packet length %d, '
|
||||
'reading data left:', packet_len)
|
||||
while True:
|
||||
try:
|
||||
__log__.error(repr(self.read(1)))
|
||||
except TimeoutError:
|
||||
break
|
||||
# Connection reset and hope it's fixed after
|
||||
self.conn.close()
|
||||
raise ConnectionResetError()
|
||||
|
||||
body = self.read(packet_len - 12)
|
||||
checksum = struct.unpack('<I', self.read(4))[0]
|
||||
|
||||
|
|
|
@ -4,10 +4,9 @@ encrypting every packet, and relies on a valid AuthKey in the used Session.
|
|||
"""
|
||||
import gzip
|
||||
import logging
|
||||
import struct
|
||||
from threading import Lock
|
||||
|
||||
from .. import helpers as utils
|
||||
from ..crypto import AES
|
||||
from ..errors import (
|
||||
BadMessageError, InvalidChecksumError, BrokenAuthKeyError,
|
||||
rpc_message_to_error
|
||||
|
@ -15,23 +14,25 @@ from ..errors import (
|
|||
from ..extensions import BinaryReader
|
||||
from ..tl import TLMessage, MessageContainer, GzipPacked
|
||||
from ..tl.all_tlobjects import tlobjects
|
||||
from ..tl.functions.auth import LogOutRequest
|
||||
from ..tl.types import (
|
||||
MsgsAck, Pong, BadServerSalt, BadMsgNotification,
|
||||
MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts,
|
||||
MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo
|
||||
)
|
||||
from ..tl.functions.auth import LogOutRequest
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MtProtoSender:
|
||||
"""MTProto Mobile Protocol sender
|
||||
(https://core.telegram.org/mtproto/description).
|
||||
"""
|
||||
MTProto Mobile Protocol sender
|
||||
(https://core.telegram.org/mtproto/description).
|
||||
|
||||
Note that this class is not thread-safe, and calling send/receive
|
||||
from two or more threads at the same time is undefined behaviour.
|
||||
Rationale: a new connection should be spawned to send/receive requests
|
||||
in parallel, so thread-safety (hence locking) isn't needed.
|
||||
Note that this class is not thread-safe, and calling send/receive
|
||||
from two or more threads at the same time is undefined behaviour.
|
||||
Rationale:
|
||||
a new connection should be spawned to send/receive requests
|
||||
in parallel, so thread-safety (hence locking) isn't needed.
|
||||
"""
|
||||
|
||||
def __init__(self, session, connection):
|
||||
|
@ -53,6 +54,9 @@ class MtProtoSender:
|
|||
# Requests (as msg_id: Message) sent waiting to be received
|
||||
self._pending_receive = {}
|
||||
|
||||
# Multithreading
|
||||
self._send_lock = Lock()
|
||||
|
||||
def connect(self):
|
||||
"""Connects to the server."""
|
||||
self.connection.connect(self.session.server_address, self.session.port)
|
||||
|
@ -67,14 +71,11 @@ class MtProtoSender:
|
|||
|
||||
def disconnect(self):
|
||||
"""Disconnects from the server."""
|
||||
__log__.info('Disconnecting MtProtoSender...')
|
||||
self.connection.close()
|
||||
self._need_confirmation.clear()
|
||||
self._clear_all_pending()
|
||||
|
||||
def clone(self):
|
||||
"""Creates a copy of this MtProtoSender as a new connection."""
|
||||
return MtProtoSender(self.session, self.connection.clone())
|
||||
|
||||
# region Send and receive
|
||||
|
||||
def send(self, *requests):
|
||||
|
@ -88,6 +89,11 @@ class MtProtoSender:
|
|||
messages = [TLMessage(self.session, r) for r in requests]
|
||||
self._pending_receive.update({m.msg_id: m for m in messages})
|
||||
|
||||
__log__.debug('Sending requests with IDs: %s', ', '.join(
|
||||
'{}: {}'.format(m.request.__class__.__name__, m.msg_id)
|
||||
for m in messages
|
||||
))
|
||||
|
||||
# Pack everything in the same container if we need to send AckRequests
|
||||
if self._need_confirmation:
|
||||
messages.append(
|
||||
|
@ -156,17 +162,8 @@ class MtProtoSender:
|
|||
|
||||
:param message: the TLMessage to be sent.
|
||||
"""
|
||||
plain_text = \
|
||||
struct.pack('<qq', self.session.salt, self.session.id) \
|
||||
+ bytes(message)
|
||||
|
||||
msg_key = utils.calc_msg_key(plain_text)
|
||||
key_id = struct.pack('<Q', self.session.auth_key.key_id)
|
||||
key, iv = utils.calc_key(self.session.auth_key.key, msg_key, True)
|
||||
cipher_text = AES.encrypt_ige(plain_text, key, iv)
|
||||
|
||||
result = key_id + msg_key + cipher_text
|
||||
self.connection.send(result)
|
||||
with self._send_lock:
|
||||
self.connection.send(utils.pack_message(self.session, message))
|
||||
|
||||
def _decode_msg(self, body):
|
||||
"""
|
||||
|
@ -175,34 +172,14 @@ class MtProtoSender:
|
|||
:param body: the body to be decoded.
|
||||
:return: a tuple of (decoded message, remote message id, remote seq).
|
||||
"""
|
||||
message = None
|
||||
remote_msg_id = None
|
||||
remote_sequence = None
|
||||
if len(body) < 8:
|
||||
if body == b'l\xfe\xff\xff':
|
||||
raise BrokenAuthKeyError()
|
||||
else:
|
||||
raise BufferError("Can't decode packet ({})".format(body))
|
||||
|
||||
with BinaryReader(body) as reader:
|
||||
if len(body) < 8:
|
||||
if body == b'l\xfe\xff\xff':
|
||||
raise BrokenAuthKeyError()
|
||||
else:
|
||||
raise BufferError("Can't decode packet ({})".format(body))
|
||||
|
||||
# TODO Check for both auth key ID and msg_key correctness
|
||||
reader.read_long() # remote_auth_key_id
|
||||
msg_key = reader.read(16)
|
||||
|
||||
key, iv = utils.calc_key(self.session.auth_key.key, msg_key, False)
|
||||
plain_text = AES.decrypt_ige(
|
||||
reader.read(len(body) - reader.tell_position()), key, iv)
|
||||
|
||||
with BinaryReader(plain_text) as plain_text_reader:
|
||||
plain_text_reader.read_long() # remote_salt
|
||||
plain_text_reader.read_long() # remote_session_id
|
||||
remote_msg_id = plain_text_reader.read_long()
|
||||
remote_sequence = plain_text_reader.read_int()
|
||||
msg_len = plain_text_reader.read_int()
|
||||
message = plain_text_reader.read(msg_len)
|
||||
|
||||
return message, remote_msg_id, remote_sequence
|
||||
return utils.unpack_message(self.session, reader)
|
||||
|
||||
def _process_msg(self, msg_id, sequence, reader, state):
|
||||
"""
|
||||
|
@ -270,9 +247,17 @@ class MtProtoSender:
|
|||
if r:
|
||||
r.result = True # Telegram won't send this value
|
||||
r.confirm_received.set()
|
||||
__log__.debug('Confirmed %s through ack', type(r).__name__)
|
||||
|
||||
return True
|
||||
|
||||
if isinstance(obj, FutureSalts):
|
||||
r = self._pop_request(obj.req_msg_id)
|
||||
if r:
|
||||
r.result = obj
|
||||
r.confirm_received.set()
|
||||
__log__.debug('Confirmed %s through salt', type(r).__name__)
|
||||
|
||||
# If the object isn't any of the above, then it should be an Update.
|
||||
self.session.process_entities(obj)
|
||||
if state:
|
||||
|
@ -328,6 +313,7 @@ class MtProtoSender:
|
|||
"""
|
||||
for r in self._pending_receive.values():
|
||||
r.request.confirm_received.set()
|
||||
__log__.info('Abruptly confirming %s', type(r).__name__)
|
||||
self._pending_receive.clear()
|
||||
|
||||
def _resend_request(self, msg_id):
|
||||
|
@ -357,6 +343,7 @@ class MtProtoSender:
|
|||
if request:
|
||||
request.result = pong
|
||||
request.confirm_received.set()
|
||||
__log__.debug('Confirmed %s through pong', type(request).__name__)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -422,13 +409,13 @@ class MtProtoSender:
|
|||
elif bad_msg.error_code == 32:
|
||||
# msg_seqno too low, so just pump it up by some "large" amount
|
||||
# TODO A better fix would be to start with a new fresh session ID
|
||||
self.session._sequence += 64
|
||||
self.session.sequence += 64
|
||||
__log__.info('Attempting to set the right higher sequence')
|
||||
self._resend_request(bad_msg.bad_msg_id)
|
||||
return True
|
||||
elif bad_msg.error_code == 33:
|
||||
# msg_seqno too high never seems to happen but just in case
|
||||
self.session._sequence -= 16
|
||||
self.session.sequence -= 16
|
||||
__log__.info('Attempting to set the right lower sequence')
|
||||
self._resend_request(bad_msg.bad_msg_id)
|
||||
return True
|
||||
|
@ -489,10 +476,13 @@ class MtProtoSender:
|
|||
reader.read_int(signed=False) # code
|
||||
request_id = reader.read_long()
|
||||
inner_code = reader.read_int(signed=False)
|
||||
reader.seek(-4)
|
||||
|
||||
__log__.debug('Received response for request with ID %d', request_id)
|
||||
request = self._pop_request(request_id)
|
||||
|
||||
if inner_code == 0x2144ca19: # RPC Error
|
||||
reader.seek(4)
|
||||
if self.session.report_errors and request:
|
||||
error = rpc_message_to_error(
|
||||
reader.read_int(), reader.tgread_string(),
|
||||
|
@ -509,26 +499,48 @@ class MtProtoSender:
|
|||
if request:
|
||||
request.rpc_error = error
|
||||
request.confirm_received.set()
|
||||
|
||||
__log__.debug('Confirmed %s through error %s',
|
||||
type(request).__name__, error)
|
||||
# else TODO Where should this error be reported?
|
||||
# Read may be async. Can an error not-belong to a request?
|
||||
return True # All contents were read okay
|
||||
|
||||
elif request:
|
||||
if inner_code == 0x3072cfa1: # GZip packed
|
||||
unpacked_data = gzip.decompress(reader.tgread_bytes())
|
||||
with BinaryReader(unpacked_data) as compressed_reader:
|
||||
if inner_code == GzipPacked.CONSTRUCTOR_ID:
|
||||
with BinaryReader(GzipPacked.read(reader)) as compressed_reader:
|
||||
request.on_response(compressed_reader)
|
||||
else:
|
||||
reader.seek(-4)
|
||||
request.on_response(reader)
|
||||
|
||||
self.session.process_entities(request.result)
|
||||
request.confirm_received.set()
|
||||
__log__.debug(
|
||||
'Confirmed %s through normal result %s',
|
||||
type(request).__name__, type(request.result).__name__
|
||||
)
|
||||
return True
|
||||
|
||||
# If it's really a result for RPC from previous connection
|
||||
# session, it will be skipped by the handle_container()
|
||||
__log__.warning('Lost request will be skipped')
|
||||
# session, it will be skipped by the handle_container().
|
||||
# For some reason this also seems to happen when downloading
|
||||
# photos, where the server responds with FileJpeg().
|
||||
def _try_read(r):
|
||||
try:
|
||||
return r.tgread_object()
|
||||
except Exception as e:
|
||||
return '(failed to read: {})'.format(e)
|
||||
|
||||
if inner_code == GzipPacked.CONSTRUCTOR_ID:
|
||||
with BinaryReader(GzipPacked.read(reader)) as compressed_reader:
|
||||
obj = _try_read(compressed_reader)
|
||||
else:
|
||||
obj = _try_read(reader)
|
||||
|
||||
__log__.warning(
|
||||
'Lost request (ID %d) with code %s will be skipped, contents: %s',
|
||||
request_id, hex(inner_code), obj
|
||||
)
|
||||
return False
|
||||
|
||||
def _handle_gzip_packed(self, msg_id, sequence, reader, state):
|
||||
|
|
3
telethon/sessions/__init__.py
Normal file
3
telethon/sessions/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .abstract import Session
|
||||
from .memory import MemorySession
|
||||
from .sqlite import SQLiteSession
|
249
telethon/sessions/abstract.py
Normal file
249
telethon/sessions/abstract.py
Normal file
|
@ -0,0 +1,249 @@
|
|||
from abc import ABC, abstractmethod
|
||||
import time
|
||||
import struct
|
||||
import os
|
||||
|
||||
|
||||
class Session(ABC):
|
||||
def __init__(self):
|
||||
# Session IDs can be random on every connection
|
||||
self.id = struct.unpack('q', os.urandom(8))[0]
|
||||
|
||||
self._sequence = 0
|
||||
self._last_msg_id = 0
|
||||
self._time_offset = 0
|
||||
self._salt = 0
|
||||
self._report_errors = True
|
||||
self._flood_sleep_threshold = 60
|
||||
|
||||
def clone(self, to_instance=None):
|
||||
"""
|
||||
Creates a clone of this session file.
|
||||
"""
|
||||
cloned = to_instance or self.__class__()
|
||||
cloned._report_errors = self.report_errors
|
||||
cloned._flood_sleep_threshold = self.flood_sleep_threshold
|
||||
return cloned
|
||||
|
||||
@abstractmethod
|
||||
def set_dc(self, dc_id, server_address, port):
|
||||
"""
|
||||
Sets the information of the data center address and port that
|
||||
the library should connect to, as well as the data center ID,
|
||||
which is currently unused.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def server_address(self):
|
||||
"""
|
||||
Returns the server address where the library should connect to.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def port(self):
|
||||
"""
|
||||
Returns the port to which the library should connect to.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def auth_key(self):
|
||||
"""
|
||||
Returns an ``AuthKey`` instance associated with the saved
|
||||
data center, or ``None`` if a new one should be generated.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@auth_key.setter
|
||||
@abstractmethod
|
||||
def auth_key(self, value):
|
||||
"""
|
||||
Sets the ``AuthKey`` to be used for the saved data center.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def close(self):
|
||||
"""
|
||||
Called on client disconnection. Should be used to
|
||||
free any used resources. Can be left empty if none.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def save(self):
|
||||
"""
|
||||
Called whenever important properties change. It should
|
||||
make persist the relevant session information to disk.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def delete(self):
|
||||
"""
|
||||
Called upon client.log_out(). Should delete the stored
|
||||
information from disk since it's not valid anymore.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def list_sessions(cls):
|
||||
"""
|
||||
Lists available sessions. Not used by the library itself.
|
||||
"""
|
||||
return []
|
||||
|
||||
@abstractmethod
|
||||
def process_entities(self, tlo):
|
||||
"""
|
||||
Processes the input ``TLObject`` or ``list`` and saves
|
||||
whatever information is relevant (e.g., ID or access hash).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_input_entity(self, key):
|
||||
"""
|
||||
Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``).
|
||||
The library uses this method whenever an ``InputPeer`` is needed
|
||||
to suit several purposes (e.g. user only provided its ID or wishes
|
||||
to use a cached username to avoid extra RPC).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def cache_file(self, md5_digest, file_size, instance):
|
||||
"""
|
||||
Caches the given file information persistently, so that it
|
||||
doesn't need to be re-uploaded in case the file is used again.
|
||||
|
||||
The ``instance`` will be either an ``InputPhoto`` or ``InputDocument``,
|
||||
both with an ``.id`` and ``.access_hash`` attributes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_file(self, md5_digest, file_size, cls):
|
||||
"""
|
||||
Returns an instance of ``cls`` if the ``md5_digest`` and ``file_size``
|
||||
match an existing saved record. The class will either be an
|
||||
``InputPhoto`` or ``InputDocument``, both with two parameters
|
||||
``id`` and ``access_hash`` in that order.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def salt(self):
|
||||
"""
|
||||
Returns the current salt used when encrypting messages.
|
||||
"""
|
||||
return self._salt
|
||||
|
||||
@salt.setter
|
||||
def salt(self, value):
|
||||
"""
|
||||
Updates the salt (integer) used when encrypting messages.
|
||||
"""
|
||||
self._salt = value
|
||||
|
||||
@property
|
||||
def report_errors(self):
|
||||
"""
|
||||
Whether RPC errors should be reported
|
||||
to https://rpc.pwrtelegram.xyz or not.
|
||||
"""
|
||||
return self._report_errors
|
||||
|
||||
@report_errors.setter
|
||||
def report_errors(self, value):
|
||||
"""
|
||||
Sets the boolean value that indicates whether RPC errors
|
||||
should be reported to https://rpc.pwrtelegram.xyz or not.
|
||||
"""
|
||||
self._report_errors = value
|
||||
|
||||
@property
|
||||
def time_offset(self):
|
||||
"""
|
||||
Time offset (in seconds) to be used
|
||||
in case the local time is incorrect.
|
||||
"""
|
||||
return self._time_offset
|
||||
|
||||
@time_offset.setter
|
||||
def time_offset(self, value):
|
||||
"""
|
||||
Updates the integer time offset in seconds.
|
||||
"""
|
||||
self._time_offset = value
|
||||
|
||||
@property
|
||||
def flood_sleep_threshold(self):
|
||||
"""
|
||||
Threshold below which the library should automatically sleep
|
||||
whenever a FloodWaitError occurs to prevent it from raising.
|
||||
"""
|
||||
return self._flood_sleep_threshold
|
||||
|
||||
@flood_sleep_threshold.setter
|
||||
def flood_sleep_threshold(self, value):
|
||||
"""
|
||||
Sets the new time threshold (integer, float or timedelta).
|
||||
"""
|
||||
self._flood_sleep_threshold = value
|
||||
|
||||
@property
|
||||
def sequence(self):
|
||||
"""
|
||||
Current sequence number needed to generate messages.
|
||||
"""
|
||||
return self._sequence
|
||||
|
||||
@sequence.setter
|
||||
def sequence(self, value):
|
||||
"""
|
||||
Updates the sequence number (integer) value.
|
||||
"""
|
||||
self._sequence = value
|
||||
|
||||
def get_new_msg_id(self):
|
||||
"""
|
||||
Generates a new unique message ID based on the current
|
||||
time (in ms) since epoch, applying a known time offset.
|
||||
"""
|
||||
now = time.time() + self._time_offset
|
||||
nanoseconds = int((now - int(now)) * 1e+9)
|
||||
new_msg_id = (int(now) << 32) | (nanoseconds << 2)
|
||||
|
||||
if self._last_msg_id >= new_msg_id:
|
||||
new_msg_id = self._last_msg_id + 4
|
||||
|
||||
self._last_msg_id = new_msg_id
|
||||
|
||||
return new_msg_id
|
||||
|
||||
def update_time_offset(self, correct_msg_id):
|
||||
"""
|
||||
Updates the time offset to the correct
|
||||
one given a known valid message ID.
|
||||
"""
|
||||
now = int(time.time())
|
||||
correct = correct_msg_id >> 32
|
||||
self._time_offset = correct - now
|
||||
self._last_msg_id = 0
|
||||
|
||||
def generate_sequence(self, content_related):
|
||||
"""
|
||||
Generates the next sequence number depending on whether
|
||||
it should be for a content-related query or not.
|
||||
"""
|
||||
if content_related:
|
||||
result = self._sequence * 2 + 1
|
||||
self._sequence += 1
|
||||
return result
|
||||
else:
|
||||
return self._sequence * 2
|
323
telethon/sessions/sqlite.py
Normal file
323
telethon/sessions/sqlite.py
Normal file
|
@ -0,0 +1,323 @@
|
|||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
from base64 import b64decode
|
||||
from os.path import isfile as file_exists
|
||||
from threading import Lock, RLock
|
||||
|
||||
from .memory import MemorySession, _SentFileType
|
||||
from .. import utils
|
||||
from ..crypto import AuthKey
|
||||
from ..tl.types import (
|
||||
InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel
|
||||
)
|
||||
|
||||
EXTENSION = '.session'
|
||||
CURRENT_VERSION = 3 # database version
|
||||
|
||||
|
||||
class SQLiteSession(MemorySession):
|
||||
"""This session contains the required information to login into your
|
||||
Telegram account. NEVER give the saved JSON file to anyone, since
|
||||
they would gain instant access to all your messages and contacts.
|
||||
|
||||
If you think the session has been compromised, close all the sessions
|
||||
through an official Telegram client to revoke the authorization.
|
||||
"""
|
||||
|
||||
def __init__(self, session_id=None):
|
||||
super().__init__()
|
||||
"""session_user_id should either be a string or another Session.
|
||||
Note that if another session is given, only parameters like
|
||||
those required to init a connection will be copied.
|
||||
"""
|
||||
# These values will NOT be saved
|
||||
self.filename = ':memory:'
|
||||
self.save_entities = True
|
||||
|
||||
if session_id:
|
||||
self.filename = session_id
|
||||
if not self.filename.endswith(EXTENSION):
|
||||
self.filename += EXTENSION
|
||||
|
||||
# Cross-thread safety
|
||||
self._seq_no_lock = Lock()
|
||||
self._msg_id_lock = Lock()
|
||||
self._db_lock = RLock()
|
||||
|
||||
# Migrating from .json -> SQL
|
||||
entities = self._check_migrate_json()
|
||||
|
||||
self._conn = None
|
||||
c = self._cursor()
|
||||
c.execute("select name from sqlite_master "
|
||||
"where type='table' and name='version'")
|
||||
if c.fetchone():
|
||||
# Tables already exist, check for the version
|
||||
c.execute("select version from version")
|
||||
version = c.fetchone()[0]
|
||||
if version != CURRENT_VERSION:
|
||||
self._upgrade_database(old=version)
|
||||
c.execute("delete from version")
|
||||
c.execute("insert into version values (?)", (CURRENT_VERSION,))
|
||||
self.save()
|
||||
|
||||
# These values will be saved
|
||||
c.execute('select * from sessions')
|
||||
tuple_ = c.fetchone()
|
||||
if tuple_:
|
||||
self._dc_id, self._server_address, self._port, key, = tuple_
|
||||
self._auth_key = AuthKey(data=key)
|
||||
|
||||
c.close()
|
||||
else:
|
||||
# Tables don't exist, create new ones
|
||||
self._create_table(
|
||||
c,
|
||||
"version (version integer primary key)"
|
||||
,
|
||||
"""sessions (
|
||||
dc_id integer primary key,
|
||||
server_address text,
|
||||
port integer,
|
||||
auth_key blob
|
||||
)"""
|
||||
,
|
||||
"""entities (
|
||||
id integer primary key,
|
||||
hash integer not null,
|
||||
username text,
|
||||
phone integer,
|
||||
name text
|
||||
)"""
|
||||
,
|
||||
"""sent_files (
|
||||
md5_digest blob,
|
||||
file_size integer,
|
||||
type integer,
|
||||
id integer,
|
||||
hash integer,
|
||||
primary key(md5_digest, file_size, type)
|
||||
)"""
|
||||
)
|
||||
c.execute("insert into version values (?)", (CURRENT_VERSION,))
|
||||
# Migrating from JSON -> new table and may have entities
|
||||
if entities:
|
||||
c.executemany(
|
||||
'insert or replace into entities values (?,?,?,?,?)',
|
||||
entities
|
||||
)
|
||||
self._update_session_table()
|
||||
c.close()
|
||||
self.save()
|
||||
|
||||
def clone(self, to_instance=None):
|
||||
cloned = super().clone(to_instance)
|
||||
cloned.save_entities = self.save_entities
|
||||
return cloned
|
||||
|
||||
def _check_migrate_json(self):
|
||||
if file_exists(self.filename):
|
||||
try:
|
||||
with open(self.filename, encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self.delete() # Delete JSON file to create database
|
||||
|
||||
self._port = data.get('port', self._port)
|
||||
self._server_address = \
|
||||
data.get('server_address', self._server_address)
|
||||
|
||||
if data.get('auth_key_data', None) is not None:
|
||||
key = b64decode(data['auth_key_data'])
|
||||
self._auth_key = AuthKey(data=key)
|
||||
|
||||
rows = []
|
||||
for p_id, p_hash in data.get('entities', []):
|
||||
if p_hash is not None:
|
||||
rows.append((p_id, p_hash, None, None, None))
|
||||
return rows
|
||||
except UnicodeDecodeError:
|
||||
return [] # No entities
|
||||
|
||||
def _upgrade_database(self, old):
|
||||
c = self._cursor()
|
||||
# old == 1 doesn't have the old sent_files so no need to drop
|
||||
if old == 2:
|
||||
# Old cache from old sent_files lasts then a day anyway, drop
|
||||
c.execute('drop table sent_files')
|
||||
self._create_table(c, """sent_files (
|
||||
md5_digest blob,
|
||||
file_size integer,
|
||||
type integer,
|
||||
id integer,
|
||||
hash integer,
|
||||
primary key(md5_digest, file_size, type)
|
||||
)""")
|
||||
c.close()
|
||||
|
||||
@staticmethod
|
||||
def _create_table(c, *definitions):
|
||||
"""
|
||||
Creates a table given its definition 'name (columns).
|
||||
If the sqlite version is >= 3.8.2, it will use "without rowid".
|
||||
See http://www.sqlite.org/releaselog/3_8_2.html.
|
||||
"""
|
||||
required = (3, 8, 2)
|
||||
sqlite_v = tuple(int(x) for x in sqlite3.sqlite_version.split('.'))
|
||||
extra = ' without rowid' if sqlite_v >= required else ''
|
||||
for definition in definitions:
|
||||
c.execute('create table {}{}'.format(definition, extra))
|
||||
|
||||
# Data from sessions should be kept as properties
|
||||
# not to fetch the database every time we need it
|
||||
def set_dc(self, dc_id, server_address, port):
|
||||
super().set_dc(dc_id, server_address, port)
|
||||
self._update_session_table()
|
||||
|
||||
# Fetch the auth_key corresponding to this data center
|
||||
c = self._cursor()
|
||||
c.execute('select auth_key from sessions')
|
||||
tuple_ = c.fetchone()
|
||||
if tuple_ and tuple_[0]:
|
||||
self._auth_key = AuthKey(data=tuple_[0])
|
||||
else:
|
||||
self._auth_key = None
|
||||
c.close()
|
||||
|
||||
@MemorySession.auth_key.setter
|
||||
def auth_key(self, value):
|
||||
self._auth_key = value
|
||||
self._update_session_table()
|
||||
|
||||
def _update_session_table(self):
|
||||
with self._db_lock:
|
||||
c = self._cursor()
|
||||
# While we can save multiple rows into the sessions table
|
||||
# currently we only want to keep ONE as the tables don't
|
||||
# tell us which auth_key's are usable and will work. Needs
|
||||
# some more work before being able to save auth_key's for
|
||||
# multiple DCs. Probably done differently.
|
||||
c.execute('delete from sessions')
|
||||
c.execute('insert or replace into sessions values (?,?,?,?)', (
|
||||
self._dc_id,
|
||||
self._server_address,
|
||||
self._port,
|
||||
self._auth_key.key if self._auth_key else b''
|
||||
))
|
||||
c.close()
|
||||
|
||||
def save(self):
|
||||
"""Saves the current session object as session_user_id.session"""
|
||||
with self._db_lock:
|
||||
self._conn.commit()
|
||||
|
||||
def _cursor(self):
|
||||
"""Asserts that the connection is open and returns a cursor"""
|
||||
with self._db_lock:
|
||||
if self._conn is None:
|
||||
self._conn = sqlite3.connect(self.filename,
|
||||
check_same_thread=False)
|
||||
return self._conn.cursor()
|
||||
|
||||
def close(self):
|
||||
"""Closes the connection unless we're working in-memory"""
|
||||
if self.filename != ':memory:':
|
||||
with self._db_lock:
|
||||
if self._conn is not None:
|
||||
self._conn.close()
|
||||
self._conn = None
|
||||
|
||||
def delete(self):
|
||||
"""Deletes the current session file"""
|
||||
if self.filename == ':memory:':
|
||||
return True
|
||||
try:
|
||||
os.remove(self.filename)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def list_sessions(cls):
|
||||
"""Lists all the sessions of the users who have ever connected
|
||||
using this client and never logged out
|
||||
"""
|
||||
return [os.path.splitext(os.path.basename(f))[0]
|
||||
for f in os.listdir('.') if f.endswith(EXTENSION)]
|
||||
|
||||
# Entity processing
|
||||
|
||||
def process_entities(self, tlo):
|
||||
"""Processes all the found entities on the given TLObject,
|
||||
unless .enabled is False.
|
||||
|
||||
Returns True if new input entities were added.
|
||||
"""
|
||||
if not self.save_entities:
|
||||
return
|
||||
|
||||
rows = self._entities_to_rows(tlo)
|
||||
if not rows:
|
||||
return
|
||||
|
||||
with self._db_lock:
|
||||
self._cursor().executemany(
|
||||
'insert or replace into entities values (?,?,?,?,?)', rows
|
||||
)
|
||||
self.save()
|
||||
|
||||
def _fetchone_entity(self, query, args):
|
||||
c = self._cursor()
|
||||
c.execute(query, args)
|
||||
return c.fetchone()
|
||||
|
||||
def get_entity_rows_by_phone(self, phone):
|
||||
return self._fetchone_entity(
|
||||
'select id, hash from entities where phone=?', (phone,))
|
||||
|
||||
def get_entity_rows_by_username(self, username):
|
||||
return self._fetchone_entity(
|
||||
'select id, hash from entities where username=?', (username,))
|
||||
|
||||
def get_entity_rows_by_name(self, name):
|
||||
return self._fetchone_entity(
|
||||
'select id, hash from entities where name=?', (name,))
|
||||
|
||||
def get_entity_rows_by_id(self, id, exact=True):
|
||||
if exact:
|
||||
return self._fetchone_entity(
|
||||
'select id, hash from entities where id=?', (id,))
|
||||
else:
|
||||
ids = (
|
||||
utils.get_peer_id(PeerUser(id)),
|
||||
utils.get_peer_id(PeerChat(id)),
|
||||
utils.get_peer_id(PeerChannel(id))
|
||||
)
|
||||
return self._fetchone_entity(
|
||||
'select id, hash from entities where id in (?,?,?)', ids
|
||||
)
|
||||
|
||||
# File processing
|
||||
|
||||
def get_file(self, md5_digest, file_size, cls):
|
||||
tuple_ = self._cursor().execute(
|
||||
'select id, hash from sent_files '
|
||||
'where md5_digest = ? and file_size = ? and type = ?',
|
||||
(md5_digest, file_size, _SentFileType.from_type(cls).value)
|
||||
).fetchone()
|
||||
if tuple_:
|
||||
# Both allowed classes have (id, access_hash) as parameters
|
||||
return cls(tuple_[0], tuple_[1])
|
||||
|
||||
def cache_file(self, md5_digest, file_size, instance):
|
||||
if not isinstance(instance, (InputDocument, InputPhoto)):
|
||||
raise TypeError('Cannot cache %s instance' % type(instance))
|
||||
|
||||
with self._db_lock:
|
||||
self._cursor().execute(
|
||||
'insert or replace into sent_files values (?,?,?,?,?)', (
|
||||
md5_digest, file_size,
|
||||
_SentFileType.from_type(type(instance)).value,
|
||||
instance.id, instance.access_hash
|
||||
))
|
||||
self.save()
|
|
@ -1,23 +1,21 @@
|
|||
import logging
|
||||
import os
|
||||
import platform
|
||||
import threading
|
||||
import warnings
|
||||
from datetime import timedelta, datetime
|
||||
from hashlib import md5
|
||||
from io import BytesIO
|
||||
from signal import signal, SIGINT, SIGTERM, SIGABRT
|
||||
from threading import Lock
|
||||
from time import sleep
|
||||
|
||||
from . import helpers as utils, version
|
||||
from .crypto import rsa, CdnDecrypter
|
||||
from . import version, utils
|
||||
from .crypto import rsa
|
||||
from .errors import (
|
||||
RPCError, BrokenAuthKeyError, ServerError,
|
||||
FloodWaitError, FileMigrateError, TypeNotFoundError,
|
||||
UnauthorizedError, PhoneMigrateError, NetworkMigrateError, UserMigrateError
|
||||
RPCError, BrokenAuthKeyError, ServerError, FloodWaitError,
|
||||
FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError,
|
||||
PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError
|
||||
)
|
||||
from .network import authenticator, MtProtoSender, Connection, ConnectionMode
|
||||
from .tl import TLObject, Session
|
||||
from .sessions import Session, SQLiteSession
|
||||
from .tl import TLObject
|
||||
from .tl.all_tlobjects import LAYER
|
||||
from .tl.functions import (
|
||||
InitConnectionRequest, InvokeWithLayerRequest, PingRequest
|
||||
|
@ -29,16 +27,10 @@ from .tl.functions.help import (
|
|||
GetCdnConfigRequest, GetConfigRequest
|
||||
)
|
||||
from .tl.functions.updates import GetStateRequest
|
||||
from .tl.functions.upload import (
|
||||
GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest
|
||||
)
|
||||
from .tl.types import InputFile, InputFileBig
|
||||
from .tl.types.auth import ExportedAuthorization
|
||||
from .tl.types.upload import FileCdnRedirect
|
||||
from .update_state import UpdateState
|
||||
from .utils import get_appropriated_part_size
|
||||
|
||||
|
||||
DEFAULT_DC_ID = 4
|
||||
DEFAULT_IPV4_IP = '149.154.167.51'
|
||||
DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]'
|
||||
DEFAULT_PORT = 443
|
||||
|
@ -81,35 +73,41 @@ class TelegramBareClient:
|
|||
update_workers=None,
|
||||
spawn_read_thread=False,
|
||||
timeout=timedelta(seconds=5),
|
||||
**kwargs):
|
||||
report_errors=True,
|
||||
device_model=None,
|
||||
system_version=None,
|
||||
app_version=None,
|
||||
lang_code='en',
|
||||
system_lang_code='en'):
|
||||
"""Refer to TelegramClient.__init__ for docs on this method"""
|
||||
if not api_id or not api_hash:
|
||||
raise PermissionError(
|
||||
raise ValueError(
|
||||
"Your API ID or Hash cannot be empty or None. "
|
||||
"Refer to Telethon's README.rst for more information.")
|
||||
"Refer to telethon.rtfd.io for more information.")
|
||||
|
||||
self._use_ipv6 = use_ipv6
|
||||
|
||||
|
||||
# Determine what session object we have
|
||||
if isinstance(session, str) or session is None:
|
||||
session = Session.try_load_or_create_new(session)
|
||||
session = SQLiteSession(session)
|
||||
elif not isinstance(session, Session):
|
||||
raise ValueError(
|
||||
raise TypeError(
|
||||
'The given session must be a str or a Session instance.'
|
||||
)
|
||||
|
||||
# ':' in session.server_address is True if it's an IPv6 address
|
||||
if (not session.server_address or
|
||||
(':' in session.server_address) != use_ipv6):
|
||||
session.port = DEFAULT_PORT
|
||||
session.server_address = \
|
||||
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP
|
||||
session.set_dc(
|
||||
DEFAULT_DC_ID,
|
||||
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,
|
||||
DEFAULT_PORT
|
||||
)
|
||||
|
||||
session.report_errors = report_errors
|
||||
self.session = session
|
||||
self.api_id = int(api_id)
|
||||
self.api_hash = api_hash
|
||||
if self.api_id < 20: # official apps must use obfuscated
|
||||
connection_mode = ConnectionMode.TCP_OBFUSCATED
|
||||
|
||||
# This is the main sender, which will be used from the thread
|
||||
# that calls .connect(). Every other thread will spawn a new
|
||||
|
@ -133,11 +131,12 @@ class TelegramBareClient:
|
|||
self.updates = UpdateState(workers=update_workers)
|
||||
|
||||
# Used on connection - the user may modify these and reconnect
|
||||
kwargs['app_version'] = kwargs.get('app_version', self.__version__)
|
||||
for name, value in kwargs.items():
|
||||
if not hasattr(self.session, name):
|
||||
raise ValueError('Unknown named parameter', name)
|
||||
setattr(self.session, name, value)
|
||||
system = platform.uname()
|
||||
self.device_model = device_model or system.system or 'Unknown'
|
||||
self.system_version = system_version or system.release or '1.0'
|
||||
self.app_version = app_version or self.__version__
|
||||
self.lang_code = lang_code
|
||||
self.system_lang_code = system_lang_code
|
||||
|
||||
# Despite the state of the real connection, keep track of whether
|
||||
# the user has explicitly called .connect() or .disconnect() here.
|
||||
|
@ -151,23 +150,28 @@ class TelegramBareClient:
|
|||
# Save whether the user is authorized here (a.k.a. logged in)
|
||||
self._authorized = None # None = We don't know yet
|
||||
|
||||
# Uploaded files cache so subsequent calls are instant
|
||||
self._upload_cache = {}
|
||||
# The first request must be in invokeWithLayer(initConnection(X)).
|
||||
# See https://core.telegram.org/api/invoking#saving-client-info.
|
||||
self._first_request = True
|
||||
|
||||
# Constantly read for results and updates from within the main client,
|
||||
# if the user has left enabled such option.
|
||||
self._spawn_read_thread = spawn_read_thread
|
||||
self._recv_thread = None
|
||||
|
||||
# Identifier of the main thread (the one that called .connect()).
|
||||
# This will be used to create new connections from any other thread,
|
||||
# so that requests can be sent in parallel.
|
||||
self._main_thread_ident = None
|
||||
self._idling = threading.Event()
|
||||
|
||||
# Default PingRequest delay
|
||||
self._last_ping = datetime.now()
|
||||
self._ping_delay = timedelta(minutes=1)
|
||||
|
||||
# Also have another delay for GetStateRequest.
|
||||
#
|
||||
# If the connection is kept alive for long without invoking any
|
||||
# high level request the server simply stops sending updates.
|
||||
# TODO maybe we can have ._last_request instead if any req works?
|
||||
self._last_state = datetime.now()
|
||||
self._state_delay = timedelta(hours=1)
|
||||
|
||||
# Some errors are known but there's nothing we can do from the
|
||||
# background thread. If any of these happens, call .disconnect(),
|
||||
# and raise them next time .invoke() is tried to be called.
|
||||
|
@ -194,7 +198,6 @@ class TelegramBareClient:
|
|||
__log__.info('Connecting to %s:%d...',
|
||||
self.session.server_address, self.session.port)
|
||||
|
||||
self._main_thread_ident = threading.get_ident()
|
||||
self._background_error = None # Clear previous errors
|
||||
|
||||
try:
|
||||
|
@ -224,6 +227,15 @@ class TelegramBareClient:
|
|||
self.disconnect()
|
||||
return self.connect(_sync_updates=_sync_updates)
|
||||
|
||||
except AuthKeyError as e:
|
||||
# As of late March 2018 there were two AUTH_KEY_DUPLICATED
|
||||
# reports. Retrying with a clean auth_key should fix this.
|
||||
__log__.warning('Auth key error %s. Clearing it and retrying.', e)
|
||||
self.disconnect()
|
||||
self.session.auth_key = None
|
||||
self.session.save()
|
||||
return self.connect(_sync_updates=_sync_updates)
|
||||
|
||||
except (RPCError, ConnectionError) as e:
|
||||
# Probably errors from the previous session, ignore them
|
||||
__log__.error('Connection failed due to %s', e)
|
||||
|
@ -237,11 +249,11 @@ class TelegramBareClient:
|
|||
"""Wraps query around InvokeWithLayerRequest(InitConnectionRequest())"""
|
||||
return InvokeWithLayerRequest(LAYER, InitConnectionRequest(
|
||||
api_id=self.api_id,
|
||||
device_model=self.session.device_model,
|
||||
system_version=self.session.system_version,
|
||||
app_version=self.session.app_version,
|
||||
lang_code=self.session.lang_code,
|
||||
system_lang_code=self.session.system_lang_code,
|
||||
device_model=self.device_model,
|
||||
system_version=self.system_version,
|
||||
app_version=self.app_version,
|
||||
lang_code=self.lang_code,
|
||||
system_lang_code=self.system_lang_code,
|
||||
lang_pack='', # "langPacks are for official apps only"
|
||||
query=query
|
||||
))
|
||||
|
@ -260,12 +272,9 @@ class TelegramBareClient:
|
|||
__log__.debug('Disconnecting the socket...')
|
||||
self._sender.disconnect()
|
||||
|
||||
if self._recv_thread:
|
||||
__log__.debug('Joining the read thread...')
|
||||
self._recv_thread.join()
|
||||
|
||||
# TODO Shall we clear the _exported_sessions, or may be reused?
|
||||
pass
|
||||
self._first_request = True # On reconnect it will be first again
|
||||
self.session.close()
|
||||
|
||||
def _reconnect(self, new_dc=None):
|
||||
"""If 'new_dc' is not set, only a call to .connect() will be made
|
||||
|
@ -294,8 +303,7 @@ class TelegramBareClient:
|
|||
dc = self._get_dc(new_dc)
|
||||
__log__.info('Reconnecting to new data center %s', dc)
|
||||
|
||||
self.session.server_address = dc.ip_address
|
||||
self.session.port = dc.port
|
||||
self.session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
# auth_key's are associated with a server, which has now changed
|
||||
# so it's not valid anymore. Set to None to force recreating it.
|
||||
self.session.auth_key = None
|
||||
|
@ -303,6 +311,13 @@ class TelegramBareClient:
|
|||
self.disconnect()
|
||||
return self.connect()
|
||||
|
||||
def set_proxy(self, proxy):
|
||||
"""Change the proxy used by the connections.
|
||||
"""
|
||||
if self.is_connected():
|
||||
raise RuntimeError("You can't change the proxy while connected.")
|
||||
self._sender.connection.conn.proxy = proxy
|
||||
|
||||
# endregion
|
||||
|
||||
# region Working with different connections/Data Centers
|
||||
|
@ -362,9 +377,8 @@ class TelegramBareClient:
|
|||
#
|
||||
# Construct this session with the connection parameters
|
||||
# (system version, device model...) from the current one.
|
||||
session = Session(self.session)
|
||||
session.server_address = dc.ip_address
|
||||
session.port = dc.port
|
||||
session = self.session.clone()
|
||||
session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
self._exported_sessions[dc_id] = session
|
||||
|
||||
__log__.info('Creating exported new client')
|
||||
|
@ -389,9 +403,8 @@ class TelegramBareClient:
|
|||
session = self._exported_sessions.get(cdn_redirect.dc_id)
|
||||
if not session:
|
||||
dc = self._get_dc(cdn_redirect.dc_id, cdn=True)
|
||||
session = Session(self.session)
|
||||
session.server_address = dc.ip_address
|
||||
session.port = dc.port
|
||||
session = self.session.clone()
|
||||
session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
self._exported_sessions[cdn_redirect.dc_id] = session
|
||||
|
||||
__log__.info('Creating new CDN client')
|
||||
|
@ -418,11 +431,17 @@ class TelegramBareClient:
|
|||
"""Invokes (sends) a MTProtoRequest and returns (receives) its result.
|
||||
|
||||
The invoke will be retried up to 'retries' times before raising
|
||||
ValueError().
|
||||
RuntimeError().
|
||||
"""
|
||||
if not all(isinstance(x, TLObject) and
|
||||
x.content_related for x in requests):
|
||||
raise ValueError('You can only invoke requests, not types!')
|
||||
raise TypeError('You can only invoke requests, not types!')
|
||||
|
||||
if self._background_error:
|
||||
raise self._background_error
|
||||
|
||||
for request in requests:
|
||||
request.resolve(self, utils)
|
||||
|
||||
# For logging purposes
|
||||
if len(requests) == 1:
|
||||
|
@ -432,70 +451,35 @@ class TelegramBareClient:
|
|||
len(requests), [type(x).__name__ for x in requests])
|
||||
|
||||
# Determine the sender to be used (main or a new connection)
|
||||
on_main_thread = threading.get_ident() == self._main_thread_ident
|
||||
if on_main_thread or self._on_read_thread():
|
||||
__log__.debug('Invoking %s from main thread', which)
|
||||
sender = self._sender
|
||||
update_state = self.updates
|
||||
else:
|
||||
__log__.debug('Invoking %s from background thread. '
|
||||
'Creating temporary connection', which)
|
||||
__log__.debug('Invoking %s', which)
|
||||
call_receive = \
|
||||
not self._idling.is_set() or self._reconnect_lock.locked()
|
||||
|
||||
sender = self._sender.clone()
|
||||
sender.connect()
|
||||
# We're on another connection, Telegram will resend all the
|
||||
# updates that we haven't acknowledged (potentially entering
|
||||
# an infinite loop if we're calling this in response to an
|
||||
# update event, as it would be received again and again). So
|
||||
# to avoid this we will simply not process updates on these
|
||||
# new temporary connections, as they will be sent and later
|
||||
# acknowledged over the main connection.
|
||||
update_state = None
|
||||
for retry in range(retries):
|
||||
result = self._invoke(call_receive, *requests)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
# We should call receive from this thread if there's no background
|
||||
# thread reading or if the server disconnected us and we're trying
|
||||
# to reconnect. This is because the read thread may either be
|
||||
# locked also trying to reconnect or we may be said thread already.
|
||||
call_receive = not on_main_thread or self._recv_thread is None \
|
||||
or self._reconnect_lock.locked()
|
||||
try:
|
||||
for attempt in range(retries):
|
||||
if self._background_error and on_main_thread:
|
||||
raise self._background_error
|
||||
log = __log__.info if retry == 0 else __log__.warning
|
||||
log('Invoking %s failed %d times, connecting again and retrying',
|
||||
[str(x) for x in requests], retry + 1)
|
||||
|
||||
result = self._invoke(
|
||||
sender, call_receive, update_state, *requests
|
||||
)
|
||||
if result is not None:
|
||||
return result
|
||||
sleep(1)
|
||||
# The ReadThread has priority when attempting reconnection,
|
||||
# since this thread is constantly running while __call__ is
|
||||
# only done sometimes. Here try connecting only once/retry.
|
||||
if not self._reconnect_lock.locked():
|
||||
with self._reconnect_lock:
|
||||
self._reconnect()
|
||||
|
||||
__log__.warning('Invoking %s failed %d times, '
|
||||
'reconnecting and retrying',
|
||||
[str(x) for x in requests], attempt + 1)
|
||||
sleep(1)
|
||||
# The ReadThread has priority when attempting reconnection,
|
||||
# since this thread is constantly running while __call__ is
|
||||
# only done sometimes. Here try connecting only once/retry.
|
||||
if sender == self._sender:
|
||||
if not self._reconnect_lock.locked():
|
||||
with self._reconnect_lock:
|
||||
self._reconnect()
|
||||
else:
|
||||
sender.connect()
|
||||
|
||||
raise ValueError('Number of retries reached 0.')
|
||||
finally:
|
||||
if sender != self._sender:
|
||||
sender.disconnect() # Close temporary connections
|
||||
raise RuntimeError('Number of retries reached 0 for {}.'.format(
|
||||
[type(x).__name__ for x in requests]
|
||||
))
|
||||
|
||||
# Let people use client.invoke(SomeRequest()) instead client(...)
|
||||
invoke = __call__
|
||||
|
||||
def _invoke(self, sender, call_receive, update_state, *requests):
|
||||
# We need to specify the new layer (by initializing a new
|
||||
# connection) if it has changed from the latest known one.
|
||||
init_connection = self.session.layer != LAYER
|
||||
|
||||
def _invoke(self, call_receive, *requests):
|
||||
try:
|
||||
# Ensure that we start with no previous errors (i.e. resending)
|
||||
for x in requests:
|
||||
|
@ -503,14 +487,12 @@ class TelegramBareClient:
|
|||
x.rpc_error = None
|
||||
|
||||
if not self.session.auth_key:
|
||||
# New key, we need to tell the server we're going to use
|
||||
# the latest layer and initialize the connection doing so.
|
||||
__log__.info('Need to generate new auth key before invoking')
|
||||
self._first_request = True
|
||||
self.session.auth_key, self.session.time_offset = \
|
||||
authenticator.do_authentication(self._sender.connection)
|
||||
init_connection = True
|
||||
|
||||
if init_connection:
|
||||
if self._first_request:
|
||||
__log__.info('Initializing a new connection while invoking')
|
||||
if len(requests) == 1:
|
||||
requests = [self._wrap_init_connection(requests[0])]
|
||||
|
@ -522,7 +504,7 @@ class TelegramBareClient:
|
|||
self._wrap_init_connection(GetConfigRequest())
|
||||
)
|
||||
|
||||
sender.send(*requests)
|
||||
self._sender.send(*requests)
|
||||
|
||||
if not call_receive:
|
||||
# TODO This will be slightly troublesome if we allow
|
||||
|
@ -531,33 +513,39 @@ class TelegramBareClient:
|
|||
# in which case a Lock would be required for .receive().
|
||||
for x in requests:
|
||||
x.confirm_received.wait(
|
||||
sender.connection.get_timeout()
|
||||
self._sender.connection.get_timeout()
|
||||
)
|
||||
else:
|
||||
while not all(x.confirm_received.is_set() for x in requests):
|
||||
sender.receive(update_state=update_state)
|
||||
self._sender.receive(update_state=self.updates)
|
||||
|
||||
except BrokenAuthKeyError:
|
||||
__log__.error('Authorization key seems broken and was invalid!')
|
||||
self.session.auth_key = None
|
||||
|
||||
except TypeNotFoundError as e:
|
||||
# Only occurs when we call receive. May happen when
|
||||
# we need to reconnect to another DC on login and
|
||||
# Telegram somehow sends old objects (like configOld)
|
||||
self._first_request = True
|
||||
__log__.warning('Read unknown TLObject code ({}). '
|
||||
'Setting again first_request flag.'
|
||||
.format(hex(e.invalid_constructor_id)))
|
||||
|
||||
except TimeoutError:
|
||||
__log__.warning('Invoking timed out') # We will just retry
|
||||
|
||||
except ConnectionResetError:
|
||||
except ConnectionResetError as e:
|
||||
__log__.warning('Connection was reset while invoking')
|
||||
if self._user_connected:
|
||||
# Server disconnected us, __call__ will try reconnecting.
|
||||
return None
|
||||
else:
|
||||
# User never called .connect(), so raise this error.
|
||||
raise
|
||||
raise RuntimeError('Tried to invoke without .connect()') from e
|
||||
|
||||
if init_connection:
|
||||
# We initialized the connection successfully, even if
|
||||
# a request had an RPC error we have invoked it fine.
|
||||
self.session.layer = LAYER
|
||||
self.session.save()
|
||||
# Clear the flag if we got this far
|
||||
self._first_request = False
|
||||
|
||||
try:
|
||||
raise next(x.rpc_error for x in requests if x.rpc_error)
|
||||
|
@ -580,13 +568,13 @@ class TelegramBareClient:
|
|||
# be on the very first connection (not authorized, not running),
|
||||
# but may be an issue for people who actually travel?
|
||||
self._reconnect(new_dc=e.new_dc)
|
||||
return self._invoke(sender, call_receive, update_state, *requests)
|
||||
return self._invoke(call_receive, *requests)
|
||||
|
||||
except ServerError as e:
|
||||
# Telegram is having some issues, just retry
|
||||
__log__.error('Telegram servers are having internal errors %s', e)
|
||||
|
||||
except FloodWaitError as e:
|
||||
except (FloodWaitError, FloodTestPhoneWaitError) as e:
|
||||
__log__.warning('Request invoked too often, wait %ds', e.seconds)
|
||||
if e.seconds > self.session.flood_sleep_threshold | 0:
|
||||
raise
|
||||
|
@ -600,193 +588,12 @@ class TelegramBareClient:
|
|||
(code request sent and confirmed)?"""
|
||||
return self._authorized
|
||||
|
||||
# endregion
|
||||
|
||||
# region Uploading media
|
||||
|
||||
def upload_file(self,
|
||||
file,
|
||||
part_size_kb=None,
|
||||
file_name=None,
|
||||
progress_callback=None):
|
||||
"""Uploads the specified file and returns a handle (an instance
|
||||
of InputFile or InputFileBig, as required) which can be later used.
|
||||
|
||||
Uploading a file will simply return a "handle" to the file stored
|
||||
remotely in the Telegram servers, which can be later used on. This
|
||||
will NOT upload the file to your own chat.
|
||||
|
||||
'file' may be either a file path, a byte array, or a stream.
|
||||
Note that if the file is a stream it will need to be read
|
||||
entirely into memory to tell its size first.
|
||||
|
||||
If 'progress_callback' is not None, it should be a function that
|
||||
takes two parameters, (bytes_uploaded, total_bytes).
|
||||
|
||||
Default values for the optional parameters if left as None are:
|
||||
part_size_kb = get_appropriated_part_size(file_size)
|
||||
file_name = os.path.basename(file_path)
|
||||
def get_input_entity(self, peer):
|
||||
"""
|
||||
if isinstance(file, str):
|
||||
file_size = os.path.getsize(file)
|
||||
elif isinstance(file, bytes):
|
||||
file_size = len(file)
|
||||
else:
|
||||
file = file.read()
|
||||
file_size = len(file)
|
||||
|
||||
if not part_size_kb:
|
||||
part_size_kb = get_appropriated_part_size(file_size)
|
||||
|
||||
if part_size_kb > 512:
|
||||
raise ValueError('The part size must be less or equal to 512KB')
|
||||
|
||||
part_size = int(part_size_kb * 1024)
|
||||
if part_size % 1024 != 0:
|
||||
raise ValueError('The part size must be evenly divisible by 1024')
|
||||
|
||||
# Determine whether the file is too big (over 10MB) or not
|
||||
# Telegram does make a distinction between smaller or larger files
|
||||
is_large = file_size > 10 * 1024 * 1024
|
||||
part_count = (file_size + part_size - 1) // part_size
|
||||
|
||||
file_id = utils.generate_random_long()
|
||||
hash_md5 = md5()
|
||||
|
||||
__log__.info('Uploading file of %d bytes in %d chunks of %d',
|
||||
file_size, part_count, part_size)
|
||||
stream = open(file, 'rb') if isinstance(file, str) else BytesIO(file)
|
||||
try:
|
||||
for part_index in range(part_count):
|
||||
# Read the file by in chunks of size part_size
|
||||
part = stream.read(part_size)
|
||||
|
||||
# The SavePartRequest is different depending on whether
|
||||
# the file is too large or not (over or less than 10MB)
|
||||
if is_large:
|
||||
request = SaveBigFilePartRequest(file_id, part_index,
|
||||
part_count, part)
|
||||
else:
|
||||
request = SaveFilePartRequest(file_id, part_index, part)
|
||||
|
||||
result = self(request)
|
||||
if result:
|
||||
__log__.debug('Uploaded %d/%d', part_index, part_count)
|
||||
if not is_large:
|
||||
# No need to update the hash if it's a large file
|
||||
hash_md5.update(part)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(stream.tell(), file_size)
|
||||
else:
|
||||
raise ValueError('Failed to upload file part {}.'
|
||||
.format(part_index))
|
||||
finally:
|
||||
stream.close()
|
||||
|
||||
# Set a default file name if None was specified
|
||||
if not file_name:
|
||||
if isinstance(file, str):
|
||||
file_name = os.path.basename(file)
|
||||
else:
|
||||
file_name = str(file_id)
|
||||
|
||||
if is_large:
|
||||
return InputFileBig(file_id, part_count, file_name)
|
||||
else:
|
||||
return InputFile(file_id, part_count, file_name,
|
||||
md5_checksum=hash_md5.hexdigest())
|
||||
|
||||
# endregion
|
||||
|
||||
# region Downloading media
|
||||
|
||||
def download_file(self,
|
||||
input_location,
|
||||
file,
|
||||
part_size_kb=None,
|
||||
file_size=None,
|
||||
progress_callback=None):
|
||||
"""Downloads the given InputFileLocation to file (a stream or str).
|
||||
|
||||
If 'progress_callback' is not None, it should be a function that
|
||||
takes two parameters, (bytes_downloaded, total_bytes). Note that
|
||||
'total_bytes' simply equals 'file_size', and may be None.
|
||||
Stub method, no functionality so that calling
|
||||
``.get_input_entity()`` from ``.resolve()`` doesn't fail.
|
||||
"""
|
||||
if not part_size_kb:
|
||||
if not file_size:
|
||||
part_size_kb = 64 # Reasonable default
|
||||
else:
|
||||
part_size_kb = get_appropriated_part_size(file_size)
|
||||
|
||||
part_size = int(part_size_kb * 1024)
|
||||
# https://core.telegram.org/api/files says:
|
||||
# > part_size % 1024 = 0 (divisible by 1KB)
|
||||
#
|
||||
# But https://core.telegram.org/cdn (more recent) says:
|
||||
# > limit must be divisible by 4096 bytes
|
||||
# So we just stick to the 4096 limit.
|
||||
if part_size % 4096 != 0:
|
||||
raise ValueError('The part size must be evenly divisible by 4096.')
|
||||
|
||||
if isinstance(file, str):
|
||||
# Ensure that we'll be able to download the media
|
||||
utils.ensure_parent_dir_exists(file)
|
||||
f = open(file, 'wb')
|
||||
else:
|
||||
f = file
|
||||
|
||||
# The used client will change if FileMigrateError occurs
|
||||
client = self
|
||||
cdn_decrypter = None
|
||||
|
||||
__log__.info('Downloading file in chunks of %d bytes', part_size)
|
||||
try:
|
||||
offset = 0
|
||||
while True:
|
||||
try:
|
||||
if cdn_decrypter:
|
||||
result = cdn_decrypter.get_file()
|
||||
else:
|
||||
result = client(GetFileRequest(
|
||||
input_location, offset, part_size
|
||||
))
|
||||
|
||||
if isinstance(result, FileCdnRedirect):
|
||||
__log__.info('File lives in a CDN')
|
||||
cdn_decrypter, result = \
|
||||
CdnDecrypter.prepare_decrypter(
|
||||
client, self._get_cdn_client(result), result
|
||||
)
|
||||
|
||||
except FileMigrateError as e:
|
||||
__log__.info('File lives in another DC')
|
||||
client = self._get_exported_client(e.new_dc)
|
||||
continue
|
||||
|
||||
offset += part_size
|
||||
|
||||
# If we have received no data (0 bytes), the file is over
|
||||
# So there is nothing left to download and write
|
||||
if not result.bytes:
|
||||
# Return some extra information, unless it's a CDN file
|
||||
return getattr(result, 'type', '')
|
||||
|
||||
f.write(result.bytes)
|
||||
__log__.debug('Saved %d more bytes', len(result.bytes))
|
||||
if progress_callback:
|
||||
progress_callback(f.tell(), file_size)
|
||||
finally:
|
||||
if client != self:
|
||||
client.disconnect()
|
||||
|
||||
if cdn_decrypter:
|
||||
try:
|
||||
cdn_decrypter.client.disconnect()
|
||||
except:
|
||||
pass
|
||||
if isinstance(file, str):
|
||||
f.close()
|
||||
return peer
|
||||
|
||||
# endregion
|
||||
|
||||
|
@ -798,28 +605,11 @@ class TelegramBareClient:
|
|||
otherwise it should be called manually after enabling updates.
|
||||
"""
|
||||
self.updates.process(self(GetStateRequest()))
|
||||
|
||||
def add_update_handler(self, handler):
|
||||
"""Adds an update handler (a function which takes a TLObject,
|
||||
an update, as its parameter) and listens for updates"""
|
||||
if self.updates.workers is None:
|
||||
warnings.warn(
|
||||
"You have not setup any workers, so you won't receive updates."
|
||||
" Pass update_workers=4 when creating the TelegramClient,"
|
||||
" or set client.self.updates.workers = 4"
|
||||
)
|
||||
|
||||
self.updates.handlers.append(handler)
|
||||
|
||||
def remove_update_handler(self, handler):
|
||||
self.updates.handlers.remove(handler)
|
||||
|
||||
def list_update_handlers(self):
|
||||
return self.updates.handlers[:]
|
||||
self._last_state = datetime.now()
|
||||
|
||||
# endregion
|
||||
|
||||
# Constant read
|
||||
# region Constant read
|
||||
|
||||
def _set_connected_and_authorized(self):
|
||||
self._authorized = True
|
||||
|
@ -850,8 +640,9 @@ class TelegramBareClient:
|
|||
:return:
|
||||
"""
|
||||
if self._spawn_read_thread and not self._on_read_thread():
|
||||
raise ValueError('Can only idle if spawn_read_thread=False')
|
||||
raise RuntimeError('Can only idle if spawn_read_thread=False')
|
||||
|
||||
self._idling.set()
|
||||
for sig in stop_signals:
|
||||
signal(sig, self._signal_handler)
|
||||
|
||||
|
@ -868,11 +659,15 @@ class TelegramBareClient:
|
|||
))
|
||||
self._last_ping = datetime.now()
|
||||
|
||||
if datetime.now() > self._last_state + self._state_delay:
|
||||
self._sender.send(GetStateRequest())
|
||||
self._last_state = datetime.now()
|
||||
|
||||
__log__.debug('Receiving items from the network...')
|
||||
self._sender.receive(update_state=self.updates)
|
||||
except TimeoutError:
|
||||
# No problem
|
||||
__log__.info('Receiving items from the network timed out')
|
||||
__log__.debug('Receiving items from the network timed out')
|
||||
except ConnectionResetError:
|
||||
if self._user_connected:
|
||||
__log__.error('Connection was reset while receiving '
|
||||
|
@ -881,6 +676,18 @@ class TelegramBareClient:
|
|||
while self._user_connected and not self._reconnect():
|
||||
sleep(0.1) # Retry forever, this is instant messaging
|
||||
|
||||
if self.is_connected():
|
||||
# Telegram seems to kick us every 1024 items received
|
||||
# from the network not considering things like bad salt.
|
||||
# We must execute some *high level* request (that's not
|
||||
# a ping) if we want to receive updates again.
|
||||
# TODO Test if getDifference works too (better alternative)
|
||||
self._sender.send(GetStateRequest())
|
||||
except:
|
||||
self._idling.clear()
|
||||
raise
|
||||
|
||||
self._idling.clear()
|
||||
__log__.info('Connection closed by the user, not reading anymore')
|
||||
|
||||
# By using this approach, another thread will be
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,4 @@
|
|||
from .tlobject import TLObject
|
||||
from .session import Session
|
||||
from .gzip_packed import GzipPacked
|
||||
from .tl_message import TLMessage
|
||||
from .message_container import MessageContainer
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
from .draft import Draft
|
||||
from .dialog import Dialog
|
||||
from .input_sized_file import InputSizedFile
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from . import Draft
|
||||
from .. import TLObject
|
||||
from ... import utils
|
||||
|
||||
|
||||
|
@ -7,7 +8,47 @@ class Dialog:
|
|||
Custom class that encapsulates a dialog (an open "conversation" with
|
||||
someone, a group or a channel) providing an abstraction to easily
|
||||
access the input version/normal entity/message etc. The library will
|
||||
return instances of this class when calling `client.get_dialogs()`.
|
||||
return instances of this class when calling :meth:`.get_dialogs()`.
|
||||
|
||||
Args:
|
||||
dialog (:tl:`Dialog`):
|
||||
The original ``Dialog`` instance.
|
||||
|
||||
pinned (`bool`):
|
||||
Whether this dialog is pinned to the top or not.
|
||||
|
||||
message (:tl:`Message`):
|
||||
The last message sent on this dialog. Note that this member
|
||||
will not be updated when new messages arrive, it's only set
|
||||
on creation of the instance.
|
||||
|
||||
date (`datetime`):
|
||||
The date of the last message sent on this dialog.
|
||||
|
||||
entity (`entity`):
|
||||
The entity that belongs to this dialog (user, chat or channel).
|
||||
|
||||
input_entity (:tl:`InputPeer`):
|
||||
Input version of the entity.
|
||||
|
||||
id (`int`):
|
||||
The marked ID of the entity, which is guaranteed to be unique.
|
||||
|
||||
name (`str`):
|
||||
Display name for this dialog. For chats and channels this is
|
||||
their title, and for users it's "First-Name Last-Name".
|
||||
|
||||
unread_count (`int`):
|
||||
How many messages are currently unread in this dialog. Note that
|
||||
this value won't update when new messages arrive.
|
||||
|
||||
unread_mentions_count (`int`):
|
||||
How many mentions are currently unread in this dialog. Note that
|
||||
this value won't update when new messages arrive.
|
||||
|
||||
draft (`telethon.tl.custom.draft.Draft`):
|
||||
The draft object in this dialog. It will not be ``None``,
|
||||
so you can call ``draft.set_message(...)``.
|
||||
"""
|
||||
def __init__(self, client, dialog, entities, messages):
|
||||
# Both entities and messages being dicts {ID: item}
|
||||
|
@ -17,21 +58,35 @@ class Dialog:
|
|||
self.message = messages.get(dialog.top_message, None)
|
||||
self.date = getattr(self.message, 'date', None)
|
||||
|
||||
self.entity = entities[utils.get_peer_id(dialog.peer, add_mark=True)]
|
||||
self.entity = entities[utils.get_peer_id(dialog.peer)]
|
||||
self.input_entity = utils.get_input_peer(self.entity)
|
||||
self.id = utils.get_peer_id(self.entity) # ^ May be InputPeerSelf()
|
||||
self.name = utils.get_display_name(self.entity)
|
||||
|
||||
self.unread_count = dialog.unread_count
|
||||
self.unread_mentions_count = dialog.unread_mentions_count
|
||||
|
||||
if dialog.draft:
|
||||
self.draft = Draft(client, dialog.peer, dialog.draft)
|
||||
else:
|
||||
self.draft = None
|
||||
self.draft = Draft(client, dialog.peer, dialog.draft)
|
||||
|
||||
def send_message(self, *args, **kwargs):
|
||||
"""
|
||||
Sends a message to this dialog. This is just a wrapper around
|
||||
client.send_message(dialog.input_entity, *args, **kwargs).
|
||||
``client.send_message(dialog.input_entity, *args, **kwargs)``.
|
||||
"""
|
||||
return self._client.send_message(self.input_entity, *args, **kwargs)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'_': 'Dialog',
|
||||
'name': self.name,
|
||||
'date': self.date,
|
||||
'draft': self.draft,
|
||||
'message': self.message,
|
||||
'entity': self.entity,
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return TLObject.pretty_format(self.to_dict())
|
||||
|
||||
def stringify(self):
|
||||
return TLObject.pretty_format(self.to_dict(), indent=0)
|
||||
|
|
|
@ -1,27 +1,44 @@
|
|||
import datetime
|
||||
|
||||
from .. import TLObject
|
||||
from ..functions.messages import SaveDraftRequest
|
||||
from ..types import UpdateDraftMessage
|
||||
from ..types import UpdateDraftMessage, DraftMessage
|
||||
from ...errors import RPCError
|
||||
from ...extensions import markdown
|
||||
|
||||
|
||||
class Draft:
|
||||
"""
|
||||
Custom class that encapsulates a draft on the Telegram servers, providing
|
||||
an abstraction to change the message conveniently. The library will return
|
||||
instances of this class when calling `client.get_drafts()`.
|
||||
instances of this class when calling :meth:`get_drafts()`.
|
||||
|
||||
Args:
|
||||
date (`datetime`):
|
||||
The date of the draft.
|
||||
|
||||
link_preview (`bool`):
|
||||
Whether the link preview is enabled or not.
|
||||
|
||||
reply_to_msg_id (`int`):
|
||||
The message ID that the draft will reply to.
|
||||
"""
|
||||
def __init__(self, client, peer, draft):
|
||||
self._client = client
|
||||
self._peer = peer
|
||||
if not draft:
|
||||
draft = DraftMessage('', None, None, None, None)
|
||||
|
||||
self.text = draft.message
|
||||
self._text = markdown.unparse(draft.message, draft.entities)
|
||||
self._raw_text = draft.message
|
||||
self.date = draft.date
|
||||
self.no_webpage = draft.no_webpage
|
||||
self.link_preview = not draft.no_webpage
|
||||
self.reply_to_msg_id = draft.reply_to_msg_id
|
||||
self.entities = draft.entities
|
||||
|
||||
@classmethod
|
||||
def _from_update(cls, client, update):
|
||||
if not isinstance(update, UpdateDraftMessage):
|
||||
raise ValueError(
|
||||
raise TypeError(
|
||||
'You can only create a new `Draft` from a corresponding '
|
||||
'`UpdateDraftMessage` object.'
|
||||
)
|
||||
|
@ -30,51 +47,120 @@ class Draft:
|
|||
|
||||
@property
|
||||
def entity(self):
|
||||
"""
|
||||
The entity that belongs to this dialog (user, chat or channel).
|
||||
"""
|
||||
return self._client.get_entity(self._peer)
|
||||
|
||||
@property
|
||||
def input_entity(self):
|
||||
"""
|
||||
Input version of the entity.
|
||||
"""
|
||||
return self._client.get_input_entity(self._peer)
|
||||
|
||||
def set_message(self, text, no_webpage=None, reply_to_msg_id=None, entities=None):
|
||||
@property
|
||||
def text(self):
|
||||
"""
|
||||
The markdown text contained in the draft. It will be
|
||||
empty if there is no text (and hence no draft is set).
|
||||
"""
|
||||
return self._text
|
||||
|
||||
@property
|
||||
def raw_text(self):
|
||||
"""
|
||||
The raw (text without formatting) contained in the draft.
|
||||
It will be empty if there is no text (thus draft not set).
|
||||
"""
|
||||
return self._raw_text
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
"""
|
||||
Convenience bool to determine if the draft is empty or not.
|
||||
"""
|
||||
return not self._text
|
||||
|
||||
def set_message(self, text=None, reply_to=0, parse_mode='md',
|
||||
link_preview=None):
|
||||
"""
|
||||
Changes the draft message on the Telegram servers. The changes are
|
||||
reflected in this object. Changing only individual attributes like for
|
||||
example the `reply_to_msg_id` should be done by providing the current
|
||||
values of this object, like so:
|
||||
reflected in this object.
|
||||
|
||||
draft.set_message(
|
||||
draft.text,
|
||||
no_webpage=draft.no_webpage,
|
||||
reply_to_msg_id=NEW_VALUE,
|
||||
entities=draft.entities
|
||||
)
|
||||
:param str text: New text of the draft.
|
||||
Preserved if left as None.
|
||||
|
||||
:param str text: New text of the draft
|
||||
:param bool no_webpage: Whether to attach a web page preview
|
||||
:param int reply_to_msg_id: Message id to reply to
|
||||
:param list entities: A list of formatting entities
|
||||
:return bool: `True` on success
|
||||
:param int reply_to: Message ID to reply to.
|
||||
Preserved if left as 0, erased if set to None.
|
||||
|
||||
:param bool link_preview: Whether to attach a web page preview.
|
||||
Preserved if left as None.
|
||||
|
||||
:param str parse_mode: The parse mode to be used for the text.
|
||||
:return bool: ``True`` on success.
|
||||
"""
|
||||
if text is None:
|
||||
text = self._text
|
||||
|
||||
if reply_to == 0:
|
||||
reply_to = self.reply_to_msg_id
|
||||
|
||||
if link_preview is None:
|
||||
link_preview = self.link_preview
|
||||
|
||||
raw_text, entities = self._client._parse_message_text(text, parse_mode)
|
||||
result = self._client(SaveDraftRequest(
|
||||
peer=self._peer,
|
||||
message=text,
|
||||
no_webpage=no_webpage,
|
||||
reply_to_msg_id=reply_to_msg_id,
|
||||
message=raw_text,
|
||||
no_webpage=not link_preview,
|
||||
reply_to_msg_id=reply_to,
|
||||
entities=entities
|
||||
))
|
||||
|
||||
if result:
|
||||
self.text = text
|
||||
self.no_webpage = no_webpage
|
||||
self.reply_to_msg_id = reply_to_msg_id
|
||||
self.entities = entities
|
||||
self._text = text
|
||||
self._raw_text = raw_text
|
||||
self.link_preview = link_preview
|
||||
self.reply_to_msg_id = reply_to
|
||||
self.date = datetime.datetime.now()
|
||||
|
||||
return result
|
||||
|
||||
def send(self, clear=True, parse_mode='md'):
|
||||
"""
|
||||
Sends the contents of this draft to the dialog. This is just a
|
||||
wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``.
|
||||
"""
|
||||
self._client.send_message(self._peer, self.text,
|
||||
reply_to=self.reply_to_msg_id,
|
||||
link_preview=self.link_preview,
|
||||
parse_mode=parse_mode,
|
||||
clear_draft=clear)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this draft
|
||||
:return bool: `True` on success
|
||||
Deletes this draft, and returns ``True`` on success.
|
||||
"""
|
||||
return self.set_message(text='')
|
||||
|
||||
def to_dict(self):
|
||||
try:
|
||||
entity = self.entity
|
||||
except RPCError as e:
|
||||
entity = e
|
||||
|
||||
return {
|
||||
'_': 'Draft',
|
||||
'text': self.text,
|
||||
'entity': entity,
|
||||
'date': self.date,
|
||||
'link_preview': self.link_preview,
|
||||
'reply_to_msg_id': self.reply_to_msg_id
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return TLObject.pretty_format(self.to_dict())
|
||||
|
||||
def stringify(self):
|
||||
return TLObject.pretty_format(self.to_dict(), indent=0)
|
||||
|
|
9
telethon/tl/custom/input_sized_file.py
Normal file
9
telethon/tl/custom/input_sized_file.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from ..types import InputFile
|
||||
|
||||
|
||||
class InputSizedFile(InputFile):
|
||||
"""InputFile class with two extra parameters: md5 (digest) and size"""
|
||||
def __init__(self, id_, parts, name, md5, size):
|
||||
super().__init__(id_, parts, name, md5.hexdigest())
|
||||
self.md5 = md5.digest()
|
||||
self.size = size
|
|
@ -1,252 +0,0 @@
|
|||
import re
|
||||
from threading import Lock
|
||||
|
||||
from ..tl import TLObject
|
||||
from ..tl.types import (
|
||||
User, Chat, Channel, PeerUser, PeerChat, PeerChannel,
|
||||
InputPeerUser, InputPeerChat, InputPeerChannel
|
||||
)
|
||||
from .. import utils # Keep this line the last to maybe fix #357
|
||||
|
||||
|
||||
USERNAME_RE = re.compile(
|
||||
r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?'
|
||||
)
|
||||
|
||||
|
||||
class EntityDatabase:
|
||||
def __init__(self, input_list=None, enabled=True, enabled_full=True):
|
||||
"""Creates a new entity database with an initial load of "Input"
|
||||
entities, if any.
|
||||
|
||||
If 'enabled', input entities will be saved. The whole entity
|
||||
will be saved if both 'enabled' and 'enabled_full' are True.
|
||||
"""
|
||||
self.enabled = enabled
|
||||
self.enabled_full = enabled_full
|
||||
|
||||
self._lock = Lock()
|
||||
self._entities = {} # marked_id: user|chat|channel
|
||||
|
||||
if input_list:
|
||||
# TODO For compatibility reasons some sessions were saved with
|
||||
# 'access_hash': null in the JSON session file. Drop these, as
|
||||
# it means we don't have access to such InputPeers. Issue #354.
|
||||
self._input_entities = {
|
||||
k: v for k, v in input_list if v is not None
|
||||
}
|
||||
else:
|
||||
self._input_entities = {} # marked_id: hash
|
||||
|
||||
# TODO Allow disabling some extra mappings
|
||||
self._username_id = {} # username: marked_id
|
||||
self._phone_id = {} # phone: marked_id
|
||||
|
||||
def process(self, tlobject):
|
||||
"""Processes all the found entities on the given TLObject,
|
||||
unless .enabled is False.
|
||||
|
||||
Returns True if new input entities were added.
|
||||
"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
# Save all input entities we know of
|
||||
if not isinstance(tlobject, TLObject) and hasattr(tlobject, '__iter__'):
|
||||
# This may be a list of users already for instance
|
||||
return self.expand(tlobject)
|
||||
|
||||
entities = []
|
||||
if hasattr(tlobject, 'chats') and hasattr(tlobject.chats, '__iter__'):
|
||||
entities.extend(tlobject.chats)
|
||||
if hasattr(tlobject, 'users') and hasattr(tlobject.users, '__iter__'):
|
||||
entities.extend(tlobject.users)
|
||||
|
||||
return self.expand(entities)
|
||||
|
||||
def expand(self, entities):
|
||||
"""Adds new input entities to the local database unconditionally.
|
||||
Unknown types will be ignored.
|
||||
"""
|
||||
if not entities or not self.enabled:
|
||||
return False
|
||||
|
||||
new = [] # Array of entities (User, Chat, or Channel)
|
||||
new_input = {} # Dictionary of {entity_marked_id: access_hash}
|
||||
for e in entities:
|
||||
if not isinstance(e, TLObject):
|
||||
continue
|
||||
|
||||
try:
|
||||
p = utils.get_input_peer(e, allow_self=False)
|
||||
marked_id = utils.get_peer_id(p, add_mark=True)
|
||||
|
||||
has_hash = False
|
||||
if isinstance(p, InputPeerChat):
|
||||
# Chats don't have a hash
|
||||
new_input[marked_id] = 0
|
||||
has_hash = True
|
||||
elif p.access_hash:
|
||||
# Some users and channels seem to be returned without
|
||||
# an 'access_hash', meaning Telegram doesn't want you
|
||||
# to access them. This is the reason behind ensuring
|
||||
# that the 'access_hash' is non-zero. See issue #354.
|
||||
new_input[marked_id] = p.access_hash
|
||||
has_hash = True
|
||||
|
||||
if self.enabled_full and has_hash:
|
||||
if isinstance(e, (User, Chat, Channel)):
|
||||
new.append(e)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
with self._lock:
|
||||
before = len(self._input_entities)
|
||||
self._input_entities.update(new_input)
|
||||
for e in new:
|
||||
self._add_full_entity(e)
|
||||
return len(self._input_entities) != before
|
||||
|
||||
def _add_full_entity(self, entity):
|
||||
"""Adds a "full" entity (User, Chat or Channel, not "Input*"),
|
||||
despite the value of self.enabled and self.enabled_full.
|
||||
|
||||
Not to be confused with UserFull, ChatFull, or ChannelFull,
|
||||
"full" means simply not "Input*".
|
||||
"""
|
||||
marked_id = utils.get_peer_id(
|
||||
utils.get_input_peer(entity, allow_self=False), add_mark=True
|
||||
)
|
||||
try:
|
||||
old_entity = self._entities[marked_id]
|
||||
old_entity.__dict__.update(entity.__dict__) # Keep old references
|
||||
|
||||
# Update must delete old username and phone
|
||||
username = getattr(old_entity, 'username', None)
|
||||
if username:
|
||||
del self._username_id[username.lower()]
|
||||
|
||||
phone = getattr(old_entity, 'phone', None)
|
||||
if phone:
|
||||
del self._phone_id[phone]
|
||||
except KeyError:
|
||||
# Add new entity
|
||||
self._entities[marked_id] = entity
|
||||
|
||||
# Always update username or phone if any
|
||||
username = getattr(entity, 'username', None)
|
||||
if username:
|
||||
self._username_id[username.lower()] = marked_id
|
||||
|
||||
phone = getattr(entity, 'phone', None)
|
||||
if phone:
|
||||
self._phone_id[phone] = marked_id
|
||||
|
||||
def _parse_key(self, key):
|
||||
"""Parses the given string, integer or TLObject key into a
|
||||
marked user ID ready for use on self._entities.
|
||||
|
||||
If a callable key is given, the entity will be passed to the
|
||||
function, and if it returns a true-like value, the marked ID
|
||||
for such entity will be returned.
|
||||
|
||||
Raises ValueError if it cannot be parsed.
|
||||
"""
|
||||
if isinstance(key, str):
|
||||
phone = EntityDatabase.parse_phone(key)
|
||||
try:
|
||||
if phone:
|
||||
return self._phone_id[phone]
|
||||
else:
|
||||
username, _ = EntityDatabase.parse_username(key)
|
||||
return self._username_id[username.lower()]
|
||||
except KeyError as e:
|
||||
raise ValueError() from e
|
||||
|
||||
if isinstance(key, int):
|
||||
return key # normal IDs are assumed users
|
||||
|
||||
if isinstance(key, TLObject):
|
||||
return utils.get_peer_id(key, add_mark=True)
|
||||
|
||||
if callable(key):
|
||||
for k, v in self._entities.items():
|
||||
if key(v):
|
||||
return k
|
||||
|
||||
raise ValueError()
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""See the ._parse_key() docstring for possible values of the key"""
|
||||
try:
|
||||
return self._entities[self._parse_key(key)]
|
||||
except (ValueError, KeyError) as e:
|
||||
raise KeyError(key) from e
|
||||
|
||||
def __delitem__(self, key):
|
||||
try:
|
||||
old = self._entities.pop(self._parse_key(key))
|
||||
# Try removing the username and phone (if pop didn't fail),
|
||||
# since the entity may have no username or phone, just ignore
|
||||
# errors. It should be there if we popped the entity correctly.
|
||||
try:
|
||||
del self._username_id[getattr(old, 'username', None)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
del self._phone_id[getattr(old, 'phone', None)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
except (ValueError, KeyError) as e:
|
||||
raise KeyError(key) from e
|
||||
|
||||
@staticmethod
|
||||
def parse_phone(phone):
|
||||
"""Parses the given phone, or returns None if it's invalid"""
|
||||
if isinstance(phone, int):
|
||||
return str(phone)
|
||||
else:
|
||||
phone = re.sub(r'[+()\s-]', '', str(phone))
|
||||
if phone.isdigit():
|
||||
return phone
|
||||
|
||||
@staticmethod
|
||||
def parse_username(username):
|
||||
"""Parses the given username or channel access hash, given
|
||||
a string, username or URL. Returns a tuple consisting of
|
||||
both the stripped username and whether it is a joinchat/ hash.
|
||||
"""
|
||||
username = username.strip()
|
||||
m = USERNAME_RE.match(username)
|
||||
if m:
|
||||
return username[m.end():], bool(m.group(1))
|
||||
else:
|
||||
return username, False
|
||||
|
||||
def get_input_entity(self, peer):
|
||||
try:
|
||||
i = utils.get_peer_id(peer, add_mark=True)
|
||||
h = self._input_entities[i] # we store the IDs marked
|
||||
i, k = utils.resolve_id(i) # removes the mark and returns kind
|
||||
|
||||
if k == PeerUser:
|
||||
return InputPeerUser(i, h)
|
||||
elif k == PeerChat:
|
||||
return InputPeerChat(i)
|
||||
elif k == PeerChannel:
|
||||
return InputPeerChannel(i, h)
|
||||
|
||||
except ValueError as e:
|
||||
raise KeyError(peer) from e
|
||||
raise KeyError(peer)
|
||||
|
||||
def get_input_list(self):
|
||||
return list(self._input_entities.items())
|
||||
|
||||
def clear(self, target=None):
|
||||
if target is None:
|
||||
self._entities.clear()
|
||||
else:
|
||||
del self[target]
|
|
@ -1,196 +0,0 @@
|
|||
import json
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
import time
|
||||
from base64 import b64encode, b64decode
|
||||
from os.path import isfile as file_exists
|
||||
from threading import Lock
|
||||
|
||||
from .entity_database import EntityDatabase
|
||||
from .. import helpers
|
||||
|
||||
|
||||
class Session:
|
||||
"""This session contains the required information to login into your
|
||||
Telegram account. NEVER give the saved JSON file to anyone, since
|
||||
they would gain instant access to all your messages and contacts.
|
||||
|
||||
If you think the session has been compromised, close all the sessions
|
||||
through an official Telegram client to revoke the authorization.
|
||||
"""
|
||||
def __init__(self, session_user_id):
|
||||
"""session_user_id should either be a string or another Session.
|
||||
Note that if another session is given, only parameters like
|
||||
those required to init a connection will be copied.
|
||||
"""
|
||||
# These values will NOT be saved
|
||||
if isinstance(session_user_id, Session):
|
||||
self.session_user_id = None
|
||||
|
||||
# For connection purposes
|
||||
session = session_user_id
|
||||
self.device_model = session.device_model
|
||||
self.system_version = session.system_version
|
||||
self.app_version = session.app_version
|
||||
self.lang_code = session.lang_code
|
||||
self.system_lang_code = session.system_lang_code
|
||||
self.lang_pack = session.lang_pack
|
||||
self.report_errors = session.report_errors
|
||||
self.save_entities = session.save_entities
|
||||
self.flood_sleep_threshold = session.flood_sleep_threshold
|
||||
|
||||
else: # str / None
|
||||
self.session_user_id = session_user_id
|
||||
|
||||
system = platform.uname()
|
||||
self.device_model = system.system if system.system else 'Unknown'
|
||||
self.system_version = system.release if system.release else '1.0'
|
||||
self.app_version = '1.0' # '0' will provoke error
|
||||
self.lang_code = 'en'
|
||||
self.system_lang_code = self.lang_code
|
||||
self.lang_pack = ''
|
||||
self.report_errors = True
|
||||
self.save_entities = True
|
||||
self.flood_sleep_threshold = 60
|
||||
|
||||
# Cross-thread safety
|
||||
self._seq_no_lock = Lock()
|
||||
self._msg_id_lock = Lock()
|
||||
self._save_lock = Lock()
|
||||
|
||||
self.id = helpers.generate_random_long(signed=True)
|
||||
self._sequence = 0
|
||||
self.time_offset = 0
|
||||
self._last_msg_id = 0 # Long
|
||||
|
||||
# These values will be saved
|
||||
self.server_address = None
|
||||
self.port = None
|
||||
self.auth_key = None
|
||||
self.layer = 0
|
||||
self.salt = 0 # Signed long
|
||||
self.entities = EntityDatabase() # Known and cached entities
|
||||
|
||||
def save(self):
|
||||
"""Saves the current session object as session_user_id.session"""
|
||||
if not self.session_user_id or self._save_lock.locked():
|
||||
return
|
||||
|
||||
with self._save_lock:
|
||||
with open('{}.session'.format(self.session_user_id), 'w') as file:
|
||||
out_dict = {
|
||||
'port': self.port,
|
||||
'salt': self.salt,
|
||||
'layer': self.layer,
|
||||
'server_address': self.server_address,
|
||||
'auth_key_data':
|
||||
b64encode(self.auth_key.key).decode('ascii')
|
||||
if self.auth_key else None
|
||||
}
|
||||
if self.save_entities:
|
||||
out_dict['entities'] = self.entities.get_input_list()
|
||||
|
||||
json.dump(out_dict, file)
|
||||
|
||||
def delete(self):
|
||||
"""Deletes the current session file"""
|
||||
try:
|
||||
os.remove('{}.session'.format(self.session_user_id))
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def list_sessions():
|
||||
"""Lists all the sessions of the users who have ever connected
|
||||
using this client and never logged out
|
||||
"""
|
||||
return [os.path.splitext(os.path.basename(f))[0]
|
||||
for f in os.listdir('.') if f.endswith('.session')]
|
||||
|
||||
@staticmethod
|
||||
def try_load_or_create_new(session_user_id):
|
||||
"""Loads a saved session_user_id.session or creates a new one.
|
||||
If session_user_id=None, later .save()'s will have no effect.
|
||||
"""
|
||||
if session_user_id is None:
|
||||
return Session(None)
|
||||
else:
|
||||
path = '{}.session'.format(session_user_id)
|
||||
result = Session(session_user_id)
|
||||
if not file_exists(path):
|
||||
return result
|
||||
|
||||
try:
|
||||
with open(path, 'r') as file:
|
||||
data = json.load(file)
|
||||
result.port = data.get('port', result.port)
|
||||
result.salt = data.get('salt', result.salt)
|
||||
# Keep while migrating from unsigned to signed salt
|
||||
if result.salt > 0:
|
||||
result.salt = struct.unpack(
|
||||
'q', struct.pack('Q', result.salt))[0]
|
||||
|
||||
result.layer = data.get('layer', result.layer)
|
||||
result.server_address = \
|
||||
data.get('server_address', result.server_address)
|
||||
|
||||
# FIXME We need to import the AuthKey here or otherwise
|
||||
# we get cyclic dependencies.
|
||||
from ..crypto import AuthKey
|
||||
if data.get('auth_key_data', None) is not None:
|
||||
key = b64decode(data['auth_key_data'])
|
||||
result.auth_key = AuthKey(data=key)
|
||||
|
||||
result.entities = EntityDatabase(data.get('entities', []))
|
||||
|
||||
except (json.decoder.JSONDecodeError, UnicodeDecodeError):
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def generate_sequence(self, content_related):
|
||||
"""Thread safe method to generates the next sequence number,
|
||||
based on whether it was confirmed yet or not.
|
||||
|
||||
Note that if confirmed=True, the sequence number
|
||||
will be increased by one too
|
||||
"""
|
||||
with self._seq_no_lock:
|
||||
if content_related:
|
||||
result = self._sequence * 2 + 1
|
||||
self._sequence += 1
|
||||
return result
|
||||
else:
|
||||
return self._sequence * 2
|
||||
|
||||
def get_new_msg_id(self):
|
||||
"""Generates a new unique message ID based on the current
|
||||
time (in ms) since epoch"""
|
||||
# Refer to mtproto_plain_sender.py for the original method
|
||||
now = time.time()
|
||||
nanoseconds = int((now - int(now)) * 1e+9)
|
||||
# "message identifiers are divisible by 4"
|
||||
new_msg_id = (int(now) << 32) | (nanoseconds << 2)
|
||||
|
||||
with self._msg_id_lock:
|
||||
if self._last_msg_id >= new_msg_id:
|
||||
new_msg_id = self._last_msg_id + 4
|
||||
|
||||
self._last_msg_id = new_msg_id
|
||||
|
||||
return new_msg_id
|
||||
|
||||
def update_time_offset(self, correct_msg_id):
|
||||
"""Updates the time offset based on a known correct message ID"""
|
||||
now = int(time.time())
|
||||
correct = correct_msg_id >> 32
|
||||
self.time_offset = correct - now
|
||||
|
||||
def process_entities(self, tlobject):
|
||||
try:
|
||||
if self.entities.process(tlobject):
|
||||
self.save() # Save if any new entities got added
|
||||
except:
|
||||
pass
|
|
@ -1,4 +1,5 @@
|
|||
from datetime import datetime
|
||||
import struct
|
||||
from datetime import datetime, date
|
||||
from threading import Event
|
||||
|
||||
|
||||
|
@ -6,6 +7,7 @@ class TLObject:
|
|||
def __init__(self):
|
||||
self.confirm_received = Event()
|
||||
self.rpc_error = None
|
||||
self.result = None
|
||||
|
||||
# These should be overrode
|
||||
self.content_related = False # Only requests/functions/queries are
|
||||
|
@ -18,14 +20,12 @@ class TLObject:
|
|||
"""
|
||||
if indent is None:
|
||||
if isinstance(obj, TLObject):
|
||||
return '{}({})'.format(type(obj).__name__, ', '.join(
|
||||
'{}={}'.format(k, TLObject.pretty_format(v))
|
||||
for k, v in obj.to_dict(recursive=False).items()
|
||||
))
|
||||
obj = obj.to_dict()
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return '{{{}}}'.format(', '.join(
|
||||
'{}: {}'.format(k, TLObject.pretty_format(v))
|
||||
for k, v in obj.items()
|
||||
return '{}({})'.format(obj.get('_', 'dict'), ', '.join(
|
||||
'{}={}'.format(k, TLObject.pretty_format(v))
|
||||
for k, v in obj.items() if k != '_'
|
||||
))
|
||||
elif isinstance(obj, str) or isinstance(obj, bytes):
|
||||
return repr(obj)
|
||||
|
@ -41,30 +41,28 @@ class TLObject:
|
|||
return repr(obj)
|
||||
else:
|
||||
result = []
|
||||
if isinstance(obj, TLObject) or isinstance(obj, dict):
|
||||
if isinstance(obj, dict):
|
||||
d = obj
|
||||
start, end, sep = '{', '}', ': '
|
||||
else:
|
||||
d = obj.to_dict(recursive=False)
|
||||
start, end, sep = '(', ')', '='
|
||||
result.append(type(obj).__name__)
|
||||
if isinstance(obj, TLObject):
|
||||
obj = obj.to_dict()
|
||||
|
||||
result.append(start)
|
||||
if d:
|
||||
if isinstance(obj, dict):
|
||||
result.append(obj.get('_', 'dict'))
|
||||
result.append('(')
|
||||
if obj:
|
||||
result.append('\n')
|
||||
indent += 1
|
||||
for k, v in d.items():
|
||||
for k, v in obj.items():
|
||||
if k == '_':
|
||||
continue
|
||||
result.append('\t' * indent)
|
||||
result.append(k)
|
||||
result.append(sep)
|
||||
result.append('=')
|
||||
result.append(TLObject.pretty_format(v, indent))
|
||||
result.append(',\n')
|
||||
result.pop() # last ',\n'
|
||||
indent -= 1
|
||||
result.append('\n')
|
||||
result.append('\t' * indent)
|
||||
result.append(end)
|
||||
result.append(')')
|
||||
|
||||
elif isinstance(obj, str) or isinstance(obj, bytes):
|
||||
result.append(repr(obj))
|
||||
|
@ -97,7 +95,8 @@ class TLObject:
|
|||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
else:
|
||||
raise ValueError('bytes or str expected, not', type(data))
|
||||
raise TypeError(
|
||||
'bytes or str expected, not {}'.format(type(data)))
|
||||
|
||||
r = []
|
||||
if len(data) < 254:
|
||||
|
@ -124,8 +123,44 @@ class TLObject:
|
|||
r.append(bytes(padding))
|
||||
return b''.join(r)
|
||||
|
||||
@staticmethod
|
||||
def serialize_datetime(dt):
|
||||
if not dt:
|
||||
return b'\0\0\0\0'
|
||||
|
||||
if isinstance(dt, datetime):
|
||||
dt = int(dt.timestamp())
|
||||
elif isinstance(dt, date):
|
||||
dt = int(datetime(dt.year, dt.month, dt.day).timestamp())
|
||||
elif isinstance(dt, float):
|
||||
dt = int(dt)
|
||||
|
||||
if isinstance(dt, int):
|
||||
return struct.pack('<I', dt)
|
||||
|
||||
raise TypeError('Cannot interpret "{}" as a date.'.format(dt))
|
||||
|
||||
# These are nearly always the same for all subclasses
|
||||
def on_response(self, reader):
|
||||
self.result = reader.tgread_object()
|
||||
|
||||
def __eq__(self, o):
|
||||
return isinstance(o, type(self)) and self.to_dict() == o.to_dict()
|
||||
|
||||
def __ne__(self, o):
|
||||
return not isinstance(o, type(self)) or self.to_dict() != o.to_dict()
|
||||
|
||||
def __str__(self):
|
||||
return TLObject.pretty_format(self)
|
||||
|
||||
def stringify(self):
|
||||
return TLObject.pretty_format(self, indent=0)
|
||||
|
||||
# These should be overrode
|
||||
def to_dict(self, recursive=True):
|
||||
def resolve(self, client, utils):
|
||||
pass
|
||||
|
||||
def to_dict(self):
|
||||
return {}
|
||||
|
||||
def __bytes__(self):
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import itertools
|
||||
import logging
|
||||
import pickle
|
||||
from collections import deque
|
||||
from queue import Queue, Empty
|
||||
from datetime import datetime
|
||||
from queue import Queue, Empty
|
||||
from threading import RLock, Thread
|
||||
|
||||
from . import utils
|
||||
from .tl import types as tl
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateState:
|
||||
"""Used to hold the current state of processed updates.
|
||||
To retrieve an update, .poll() should be called.
|
||||
"""
|
||||
Used to hold the current state of processed updates.
|
||||
To retrieve an update, :meth:`poll` should be called.
|
||||
"""
|
||||
WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers
|
||||
|
||||
|
@ -22,15 +23,14 @@ class UpdateState:
|
|||
workers is None: Updates will *not* be stored on self.
|
||||
workers = 0: Another thread is responsible for calling self.poll()
|
||||
workers > 0: 'workers' background threads will be spawned, any
|
||||
any of them will invoke all the self.handlers.
|
||||
any of them will invoke the self.handler.
|
||||
"""
|
||||
self._workers = workers
|
||||
self._worker_threads = []
|
||||
|
||||
self.handlers = []
|
||||
self.handler = None
|
||||
self._updates_lock = RLock()
|
||||
self._updates = Queue()
|
||||
self._latest_updates = deque(maxlen=10)
|
||||
|
||||
# https://core.telegram.org/api/updates
|
||||
self._state = tl.updates.State(0, 0, datetime.now(), 0, 0)
|
||||
|
@ -40,19 +40,15 @@ class UpdateState:
|
|||
return not self._updates.empty()
|
||||
|
||||
def poll(self, timeout=None):
|
||||
"""Polls an update or blocks until an update object is available.
|
||||
If 'timeout is not None', it should be a floating point value,
|
||||
and the method will 'return None' if waiting times out.
|
||||
"""
|
||||
Polls an update or blocks until an update object is available.
|
||||
If 'timeout is not None', it should be a floating point value,
|
||||
and the method will 'return None' if waiting times out.
|
||||
"""
|
||||
try:
|
||||
update = self._updates.get(timeout=timeout)
|
||||
return self._updates.get(timeout=timeout)
|
||||
except Empty:
|
||||
return
|
||||
|
||||
if isinstance(update, Exception):
|
||||
raise update # Some error was set through (surely StopIteration)
|
||||
|
||||
return update
|
||||
return None
|
||||
|
||||
def get_workers(self):
|
||||
return self._workers
|
||||
|
@ -61,32 +57,31 @@ class UpdateState:
|
|||
"""Changes the number of workers running.
|
||||
If 'n is None', clears all pending updates from memory.
|
||||
"""
|
||||
self.stop_workers()
|
||||
self._workers = n
|
||||
if n is None:
|
||||
while self._updates:
|
||||
self._updates.get()
|
||||
self.stop_workers()
|
||||
else:
|
||||
self._workers = n
|
||||
self.setup_workers()
|
||||
|
||||
workers = property(fget=get_workers, fset=set_workers)
|
||||
|
||||
def stop_workers(self):
|
||||
"""Raises "StopIterationException" on the worker threads to stop them,
|
||||
and also clears all of them off the list
|
||||
"""
|
||||
if self._workers:
|
||||
Waits for all the worker threads to stop.
|
||||
"""
|
||||
# Put dummy ``None`` objects so that they don't need to timeout.
|
||||
n = self._workers
|
||||
self._workers = None
|
||||
if n:
|
||||
with self._updates_lock:
|
||||
# Insert at the beginning so the very next poll causes an error
|
||||
# on all the worker threads
|
||||
# TODO Should this reset the pts and such?
|
||||
for _ in range(self._workers):
|
||||
self._updates.put(StopIteration())
|
||||
for _ in range(n):
|
||||
self._updates.put(None)
|
||||
|
||||
for t in self._worker_threads:
|
||||
t.join()
|
||||
|
||||
self._worker_threads.clear()
|
||||
self._workers = n
|
||||
|
||||
def setup_workers(self):
|
||||
if self._worker_threads or not self._workers:
|
||||
|
@ -104,13 +99,11 @@ class UpdateState:
|
|||
thread.start()
|
||||
|
||||
def _worker_loop(self, wid):
|
||||
while True:
|
||||
while self._workers is not None:
|
||||
try:
|
||||
update = self.poll(timeout=UpdateState.WORKER_POLL_TIMEOUT)
|
||||
# TODO Maybe people can add different handlers per update type
|
||||
if update:
|
||||
for handler in self.handlers:
|
||||
handler(update)
|
||||
if update and self.handler:
|
||||
self.handler(update)
|
||||
except StopIteration:
|
||||
break
|
||||
except:
|
||||
|
@ -130,42 +123,26 @@ class UpdateState:
|
|||
self._state = update
|
||||
return # Nothing else to be done
|
||||
|
||||
pts = getattr(update, 'pts', self._state.pts)
|
||||
if hasattr(update, 'pts') and pts <= self._state.pts:
|
||||
__log__.info('Ignoring %s, already have it', update)
|
||||
return # We already handled this update
|
||||
|
||||
self._state.pts = pts
|
||||
|
||||
# TODO There must be a better way to handle updates rather than
|
||||
# keeping a queue with the latest updates only, and handling
|
||||
# the 'pts' correctly should be enough. However some updates
|
||||
# like UpdateUserStatus (even inside UpdateShort) will be called
|
||||
# repeatedly very often if invoking anything inside an update
|
||||
# handler. TODO Figure out why.
|
||||
"""
|
||||
client = TelegramClient('anon', api_id, api_hash, update_workers=1)
|
||||
client.connect()
|
||||
def handle(u):
|
||||
client.get_me()
|
||||
client.add_update_handler(handle)
|
||||
input('Enter to exit.')
|
||||
"""
|
||||
data = pickle.dumps(update.to_dict())
|
||||
if data in self._latest_updates:
|
||||
__log__.info('Ignoring %s, already have it', update)
|
||||
return # Duplicated too
|
||||
|
||||
self._latest_updates.append(data)
|
||||
if hasattr(update, 'pts'):
|
||||
self._state.pts = update.pts
|
||||
|
||||
# After running the script for over an hour and receiving over
|
||||
# 1000 updates, the only duplicates received were users going
|
||||
# online or offline. We can trust the server until new reports.
|
||||
# This should only be used as read-only.
|
||||
if isinstance(update, tl.UpdateShort):
|
||||
update.update._entities = {}
|
||||
self._updates.put(update.update)
|
||||
# Expand "Updates" into "Update", and pass these to callbacks.
|
||||
# Since .users and .chats have already been processed, we
|
||||
# don't need to care about those either.
|
||||
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
|
||||
entities = {utils.get_peer_id(x): x for x in
|
||||
itertools.chain(update.users, update.chats)}
|
||||
for u in update.updates:
|
||||
u._entities = entities
|
||||
self._updates.put(u)
|
||||
# TODO Handle "tl.UpdatesTooLong"
|
||||
else:
|
||||
update._entities = {}
|
||||
self._updates.put(update)
|
||||
|
|
|
@ -3,9 +3,13 @@ Utilities for working with the Telegram API itself (such as handy methods
|
|||
to convert between an entity like an User, Chat, etc. into its Input version)
|
||||
"""
|
||||
import math
|
||||
from mimetypes import add_type, guess_extension
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import types
|
||||
from collections import UserList
|
||||
from mimetypes import guess_extension
|
||||
|
||||
from .tl import TLObject
|
||||
from .tl.types import (
|
||||
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
||||
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
|
||||
|
@ -20,13 +24,23 @@ from .tl.types import (
|
|||
GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty,
|
||||
InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty,
|
||||
FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull,
|
||||
InputMediaUploadedPhoto, DocumentAttributeFilename, photos
|
||||
InputMediaUploadedPhoto, DocumentAttributeFilename, photos,
|
||||
TopPeer, InputNotifyPeer
|
||||
)
|
||||
from .tl.types.contacts import ResolvedPeer
|
||||
|
||||
USERNAME_RE = re.compile(
|
||||
r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?'
|
||||
)
|
||||
|
||||
VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$')
|
||||
|
||||
|
||||
def get_display_name(entity):
|
||||
"""Gets the input peer for the given "entity" (user, chat or channel)
|
||||
Returns None if it was not found"""
|
||||
"""
|
||||
Gets the display name for the given entity, if it's an :tl:`User`,
|
||||
:tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise.
|
||||
"""
|
||||
if isinstance(entity, User):
|
||||
if entity.last_name and entity.first_name:
|
||||
return '{} {}'.format(entity.first_name, entity.last_name)
|
||||
|
@ -42,12 +56,9 @@ def get_display_name(entity):
|
|||
|
||||
return ''
|
||||
|
||||
# For some reason, .webp (stickers' format) is not registered
|
||||
add_type('image/webp', '.webp')
|
||||
|
||||
|
||||
def get_extension(media):
|
||||
"""Gets the corresponding extension for any Telegram media"""
|
||||
"""Gets the corresponding extension for any Telegram media."""
|
||||
|
||||
# Photos are always compressed as .jpg by Telegram
|
||||
if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)):
|
||||
|
@ -55,31 +66,33 @@ def get_extension(media):
|
|||
|
||||
# Documents will come with a mime type
|
||||
if isinstance(media, MessageMediaDocument):
|
||||
if isinstance(media.document, Document):
|
||||
if media.document.mime_type == 'application/octet-stream':
|
||||
# Octet stream are just bytes, which have no default extension
|
||||
return ''
|
||||
else:
|
||||
extension = guess_extension(media.document.mime_type)
|
||||
return extension if extension else ''
|
||||
media = media.document
|
||||
if isinstance(media, Document):
|
||||
if media.mime_type == 'application/octet-stream':
|
||||
# Octet stream are just bytes, which have no default extension
|
||||
return ''
|
||||
else:
|
||||
return guess_extension(media.mime_type) or ''
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
def _raise_cast_fail(entity, target):
|
||||
raise ValueError('Cannot cast {} to any kind of {}.'
|
||||
.format(type(entity).__name__, target))
|
||||
raise TypeError('Cannot cast {} to any kind of {}.'.format(
|
||||
type(entity).__name__, target))
|
||||
|
||||
|
||||
def get_input_peer(entity, allow_self=True):
|
||||
"""Gets the input peer for the given "entity" (user, chat or channel).
|
||||
A ValueError is raised if the given entity isn't a supported type."""
|
||||
if not isinstance(entity, TLObject):
|
||||
"""
|
||||
Gets the input peer for the given "entity" (user, chat or channel).
|
||||
A ``TypeError`` is raised if the given entity isn't a supported type.
|
||||
"""
|
||||
try:
|
||||
if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
|
||||
return entity
|
||||
except AttributeError:
|
||||
_raise_cast_fail(entity, 'InputPeer')
|
||||
|
||||
if type(entity).SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
|
||||
return entity
|
||||
|
||||
if isinstance(entity, User):
|
||||
if entity.is_self and allow_self:
|
||||
return InputPeerSelf()
|
||||
|
@ -93,15 +106,18 @@ def get_input_peer(entity, allow_self=True):
|
|||
return InputPeerChannel(entity.id, entity.access_hash or 0)
|
||||
|
||||
# Less common cases
|
||||
if isinstance(entity, UserEmpty):
|
||||
return InputPeerEmpty()
|
||||
|
||||
if isinstance(entity, InputUser):
|
||||
return InputPeerUser(entity.user_id, entity.access_hash)
|
||||
|
||||
if isinstance(entity, InputChannel):
|
||||
return InputPeerChannel(entity.channel_id, entity.access_hash)
|
||||
|
||||
if isinstance(entity, InputUserSelf):
|
||||
return InputPeerSelf()
|
||||
|
||||
if isinstance(entity, UserEmpty):
|
||||
return InputPeerEmpty()
|
||||
|
||||
if isinstance(entity, UserFull):
|
||||
return get_input_peer(entity.user)
|
||||
|
||||
|
@ -115,13 +131,13 @@ def get_input_peer(entity, allow_self=True):
|
|||
|
||||
|
||||
def get_input_channel(entity):
|
||||
"""Similar to get_input_peer, but for InputChannel's alone"""
|
||||
if not isinstance(entity, TLObject):
|
||||
"""Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone."""
|
||||
try:
|
||||
if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
|
||||
return entity
|
||||
except AttributeError:
|
||||
_raise_cast_fail(entity, 'InputChannel')
|
||||
|
||||
if type(entity).SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
|
||||
return entity
|
||||
|
||||
if isinstance(entity, (Channel, ChannelForbidden)):
|
||||
return InputChannel(entity.id, entity.access_hash or 0)
|
||||
|
||||
|
@ -132,13 +148,13 @@ def get_input_channel(entity):
|
|||
|
||||
|
||||
def get_input_user(entity):
|
||||
"""Similar to get_input_peer, but for InputUser's alone"""
|
||||
if not isinstance(entity, TLObject):
|
||||
"""Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone."""
|
||||
try:
|
||||
if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'):
|
||||
return entity
|
||||
except AttributeError:
|
||||
_raise_cast_fail(entity, 'InputUser')
|
||||
|
||||
if type(entity).SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser')
|
||||
return entity
|
||||
|
||||
if isinstance(entity, User):
|
||||
if entity.is_self:
|
||||
return InputUserSelf()
|
||||
|
@ -161,13 +177,13 @@ def get_input_user(entity):
|
|||
|
||||
|
||||
def get_input_document(document):
|
||||
"""Similar to get_input_peer, but for documents"""
|
||||
if not isinstance(document, TLObject):
|
||||
"""Similar to :meth:`get_input_peer`, but for documents"""
|
||||
try:
|
||||
if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'):
|
||||
return document
|
||||
except AttributeError:
|
||||
_raise_cast_fail(document, 'InputDocument')
|
||||
|
||||
if type(document).SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument')
|
||||
return document
|
||||
|
||||
if isinstance(document, Document):
|
||||
return InputDocument(id=document.id, access_hash=document.access_hash)
|
||||
|
||||
|
@ -184,13 +200,13 @@ def get_input_document(document):
|
|||
|
||||
|
||||
def get_input_photo(photo):
|
||||
"""Similar to get_input_peer, but for documents"""
|
||||
if not isinstance(photo, TLObject):
|
||||
"""Similar to :meth:`get_input_peer`, but for photos"""
|
||||
try:
|
||||
if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'):
|
||||
return photo
|
||||
except AttributeError:
|
||||
_raise_cast_fail(photo, 'InputPhoto')
|
||||
|
||||
if type(photo).SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
|
||||
return photo
|
||||
|
||||
if isinstance(photo, photos.Photo):
|
||||
photo = photo.photo
|
||||
|
||||
|
@ -204,13 +220,13 @@ def get_input_photo(photo):
|
|||
|
||||
|
||||
def get_input_geo(geo):
|
||||
"""Similar to get_input_peer, but for geo points"""
|
||||
if not isinstance(geo, TLObject):
|
||||
"""Similar to :meth:`get_input_peer`, but for geo points"""
|
||||
try:
|
||||
if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'):
|
||||
return geo
|
||||
except AttributeError:
|
||||
_raise_cast_fail(geo, 'InputGeoPoint')
|
||||
|
||||
if type(geo).SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint')
|
||||
return geo
|
||||
|
||||
if isinstance(geo, GeoPoint):
|
||||
return InputGeoPoint(lat=geo.lat, long=geo.long)
|
||||
|
||||
|
@ -226,44 +242,49 @@ def get_input_geo(geo):
|
|||
_raise_cast_fail(geo, 'InputGeoPoint')
|
||||
|
||||
|
||||
def get_input_media(media, user_caption=None, is_photo=False):
|
||||
"""Similar to get_input_peer, but for media.
|
||||
|
||||
If the media is a file location and is_photo is known to be True,
|
||||
it will be treated as an InputMediaUploadedPhoto.
|
||||
def get_input_media(media, is_photo=False):
|
||||
"""
|
||||
if not isinstance(media, TLObject):
|
||||
_raise_cast_fail(media, 'InputMedia')
|
||||
Similar to :meth:`get_input_peer`, but for media.
|
||||
|
||||
if type(media).SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia')
|
||||
return media
|
||||
If the media is a file location and ``is_photo`` is known to be ``True``,
|
||||
it will be treated as an :tl:`InputMediaUploadedPhoto`.
|
||||
"""
|
||||
try:
|
||||
if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia'):
|
||||
return media
|
||||
except AttributeError:
|
||||
_raise_cast_fail(media, 'InputMedia')
|
||||
|
||||
if isinstance(media, MessageMediaPhoto):
|
||||
return InputMediaPhoto(
|
||||
id=get_input_photo(media.photo),
|
||||
caption=media.caption if user_caption is None else user_caption,
|
||||
ttl_seconds=media.ttl_seconds
|
||||
)
|
||||
|
||||
if isinstance(media, (Photo, photos.Photo, PhotoEmpty)):
|
||||
return InputMediaPhoto(
|
||||
id=get_input_photo(media)
|
||||
)
|
||||
|
||||
if isinstance(media, MessageMediaDocument):
|
||||
return InputMediaDocument(
|
||||
id=get_input_document(media.document),
|
||||
caption=media.caption if user_caption is None else user_caption,
|
||||
ttl_seconds=media.ttl_seconds
|
||||
)
|
||||
|
||||
if isinstance(media, (Document, DocumentEmpty)):
|
||||
return InputMediaDocument(
|
||||
id=get_input_document(media)
|
||||
)
|
||||
|
||||
if isinstance(media, FileLocation):
|
||||
if is_photo:
|
||||
return InputMediaUploadedPhoto(
|
||||
file=media,
|
||||
caption=user_caption or ''
|
||||
)
|
||||
return InputMediaUploadedPhoto(file=media)
|
||||
else:
|
||||
return InputMediaUploadedDocument(
|
||||
file=media,
|
||||
mime_type='application/octet-stream', # unknown, assume bytes
|
||||
attributes=[DocumentAttributeFilename('unnamed')],
|
||||
caption=user_caption or ''
|
||||
attributes=[DocumentAttributeFilename('unnamed')]
|
||||
)
|
||||
|
||||
if isinstance(media, MessageMediaGame):
|
||||
|
@ -271,9 +292,10 @@ def get_input_media(media, user_caption=None, is_photo=False):
|
|||
|
||||
if isinstance(media, (ChatPhoto, UserProfilePhoto)):
|
||||
if isinstance(media.photo_big, FileLocationUnavailable):
|
||||
return get_input_media(media.photo_small, is_photo=True)
|
||||
media = media.photo_small
|
||||
else:
|
||||
return get_input_media(media.photo_big, is_photo=True)
|
||||
media = media.photo_big
|
||||
return get_input_media(media, is_photo=True)
|
||||
|
||||
if isinstance(media, MessageMediaContact):
|
||||
return InputMediaContact(
|
||||
|
@ -291,7 +313,8 @@ def get_input_media(media, user_caption=None, is_photo=False):
|
|||
title=media.title,
|
||||
address=media.address,
|
||||
provider=media.provider,
|
||||
venue_id=media.venue_id
|
||||
venue_id=media.venue_id,
|
||||
venue_type=''
|
||||
)
|
||||
|
||||
if isinstance(media, (
|
||||
|
@ -300,31 +323,122 @@ def get_input_media(media, user_caption=None, is_photo=False):
|
|||
return InputMediaEmpty()
|
||||
|
||||
if isinstance(media, Message):
|
||||
return get_input_media(media.media)
|
||||
return get_input_media(media.media, is_photo=is_photo)
|
||||
|
||||
_raise_cast_fail(media, 'InputMedia')
|
||||
|
||||
|
||||
def get_peer_id(peer, add_mark=False):
|
||||
"""Finds the ID of the given peer, and optionally converts it to
|
||||
the "bot api" format if 'add_mark' is set to True.
|
||||
def is_image(file):
|
||||
"""
|
||||
Returns ``True`` if the file extension looks like an image file to Telegram.
|
||||
"""
|
||||
if not isinstance(file, str):
|
||||
return False
|
||||
_, ext = os.path.splitext(file)
|
||||
return re.match(r'\.(png|jpe?g)', ext, re.IGNORECASE)
|
||||
|
||||
|
||||
def is_audio(file):
|
||||
"""Returns ``True`` if the file extension looks like an audio file."""
|
||||
return (isinstance(file, str) and
|
||||
(mimetypes.guess_type(file)[0] or '').startswith('audio/'))
|
||||
|
||||
|
||||
def is_video(file):
|
||||
"""Returns ``True`` if the file extension looks like a video file."""
|
||||
return (isinstance(file, str) and
|
||||
(mimetypes.guess_type(file)[0] or '').startswith('video/'))
|
||||
|
||||
|
||||
def is_list_like(obj):
|
||||
"""
|
||||
Returns ``True`` if the given object looks like a list.
|
||||
|
||||
Checking ``if hasattr(obj, '__iter__')`` and ignoring ``str/bytes`` is not
|
||||
enough. Things like ``open()`` are also iterable (and probably many
|
||||
other things), so just support the commonly known list-like objects.
|
||||
"""
|
||||
return isinstance(obj, (list, tuple, set, dict,
|
||||
UserList, types.GeneratorType))
|
||||
|
||||
|
||||
def parse_phone(phone):
|
||||
"""Parses the given phone, or returns ``None`` if it's invalid."""
|
||||
if isinstance(phone, int):
|
||||
return str(phone)
|
||||
else:
|
||||
phone = re.sub(r'[+()\s-]', '', str(phone))
|
||||
if phone.isdigit():
|
||||
return phone
|
||||
|
||||
|
||||
def parse_username(username):
|
||||
"""Parses the given username or channel access hash, given
|
||||
a string, username or URL. Returns a tuple consisting of
|
||||
both the stripped, lowercase username and whether it is
|
||||
a joinchat/ hash (in which case is not lowercase'd).
|
||||
|
||||
Returns ``None`` if the ``username`` is not valid.
|
||||
"""
|
||||
username = username.strip()
|
||||
m = USERNAME_RE.match(username)
|
||||
if m:
|
||||
username = username[m.end():]
|
||||
is_invite = bool(m.group(1))
|
||||
if is_invite:
|
||||
return username, True
|
||||
else:
|
||||
username = username.rstrip('/')
|
||||
|
||||
if VALID_USERNAME_RE.match(username):
|
||||
return username.lower(), False
|
||||
else:
|
||||
return None, False
|
||||
|
||||
|
||||
def _fix_peer_id(peer_id):
|
||||
"""
|
||||
Fixes the peer ID for chats and channels, in case the users
|
||||
mix marking the ID with the :tl:`Peer` constructors.
|
||||
"""
|
||||
peer_id = abs(peer_id)
|
||||
if str(peer_id).startswith('100'):
|
||||
peer_id = str(peer_id)[3:]
|
||||
return int(peer_id)
|
||||
|
||||
|
||||
def get_peer_id(peer):
|
||||
"""
|
||||
Finds the ID of the given peer, and converts it to the "bot api" format
|
||||
so it the peer can be identified back. User ID is left unmodified,
|
||||
chat ID is negated, and channel ID is prefixed with -100.
|
||||
|
||||
The original ID and the peer type class can be returned with
|
||||
a call to :meth:`resolve_id(marked_id)`.
|
||||
"""
|
||||
# First we assert it's a Peer TLObject, or early return for integers
|
||||
if not isinstance(peer, TLObject):
|
||||
if isinstance(peer, int):
|
||||
return peer
|
||||
else:
|
||||
_raise_cast_fail(peer, 'int')
|
||||
if isinstance(peer, int):
|
||||
return peer
|
||||
|
||||
elif type(peer).SUBCLASS_OF_ID not in {0x2d45687, 0xc91c90b6}:
|
||||
# Not a Peer or an InputPeer, so first get its Input version
|
||||
peer = get_input_peer(peer, allow_self=False)
|
||||
try:
|
||||
if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6):
|
||||
if isinstance(peer, (ResolvedPeer, InputNotifyPeer, TopPeer)):
|
||||
peer = peer.peer
|
||||
else:
|
||||
# Not a Peer or an InputPeer, so first get its Input version
|
||||
peer = get_input_peer(peer, allow_self=False)
|
||||
except AttributeError:
|
||||
_raise_cast_fail(peer, 'int')
|
||||
|
||||
# Set the right ID/kind, or raise if the TLObject is not recognised
|
||||
if isinstance(peer, (PeerUser, InputPeerUser)):
|
||||
return peer.user_id
|
||||
elif isinstance(peer, (PeerChat, InputPeerChat)):
|
||||
return -peer.chat_id if add_mark else peer.chat_id
|
||||
# Check in case the user mixed things up to avoid blowing up
|
||||
if not (0 < peer.chat_id <= 0x7fffffff):
|
||||
peer.chat_id = _fix_peer_id(peer.chat_id)
|
||||
|
||||
return -peer.chat_id
|
||||
elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)):
|
||||
if isinstance(peer, ChannelFull):
|
||||
# Special case: .get_input_peer can't return InputChannel from
|
||||
|
@ -332,18 +446,24 @@ def get_peer_id(peer, add_mark=False):
|
|||
i = peer.id
|
||||
else:
|
||||
i = peer.channel_id
|
||||
if add_mark:
|
||||
# Concat -100 through math tricks, .to_supergroup() on Madeline
|
||||
# IDs will be strictly positive -> log works
|
||||
return -(i + pow(10, math.floor(math.log10(i) + 3)))
|
||||
else:
|
||||
return i
|
||||
|
||||
# Check in case the user mixed things up to avoid blowing up
|
||||
if not (0 < i <= 0x7fffffff):
|
||||
i = _fix_peer_id(i)
|
||||
if isinstance(peer, ChannelFull):
|
||||
peer.id = i
|
||||
else:
|
||||
peer.channel_id = i
|
||||
|
||||
# Concat -100 through math tricks, .to_supergroup() on Madeline
|
||||
# IDs will be strictly positive -> log works
|
||||
return -(i + pow(10, math.floor(math.log10(i) + 3)))
|
||||
|
||||
_raise_cast_fail(peer, 'int')
|
||||
|
||||
|
||||
def resolve_id(marked_id):
|
||||
"""Given a marked ID, returns the original ID and its Peer type"""
|
||||
"""Given a marked ID, returns the original ID and its :tl:`Peer` type."""
|
||||
if marked_id >= 0:
|
||||
return marked_id, PeerUser
|
||||
|
||||
|
@ -353,31 +473,11 @@ def resolve_id(marked_id):
|
|||
return -marked_id, PeerChat
|
||||
|
||||
|
||||
def find_user_or_chat(peer, users, chats):
|
||||
"""Finds the corresponding user or chat given a peer.
|
||||
Returns None if it was not found"""
|
||||
if isinstance(peer, PeerUser):
|
||||
peer, where = peer.user_id, users
|
||||
else:
|
||||
where = chats
|
||||
if isinstance(peer, PeerChat):
|
||||
peer = peer.chat_id
|
||||
elif isinstance(peer, PeerChannel):
|
||||
peer = peer.channel_id
|
||||
|
||||
if isinstance(peer, int):
|
||||
if isinstance(where, dict):
|
||||
return where.get(peer)
|
||||
else:
|
||||
try:
|
||||
return next(x for x in where if x.id == peer)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
|
||||
def get_appropriated_part_size(file_size):
|
||||
"""Gets the appropriated part size when uploading or downloading files,
|
||||
given an initial file size"""
|
||||
"""
|
||||
Gets the appropriated part size when uploading or downloading files,
|
||||
given an initial file size.
|
||||
"""
|
||||
if file_size <= 104857600: # 100MB
|
||||
return 128
|
||||
if file_size <= 786432000: # 750MB
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '0.15.5'
|
||||
__version__ = '0.18.2'
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1,12 +1,13 @@
|
|||
import os
|
||||
from getpass import getpass
|
||||
|
||||
from telethon import TelegramClient, ConnectionMode
|
||||
from telethon.utils import get_display_name
|
||||
|
||||
from telethon import ConnectionMode, TelegramClient
|
||||
from telethon.errors import SessionPasswordNeededError
|
||||
from telethon.tl.types import (
|
||||
UpdateShortChatMessage, UpdateShortMessage, PeerChat
|
||||
PeerChat, UpdateShortChatMessage, UpdateShortMessage
|
||||
)
|
||||
from telethon.utils import get_display_name
|
||||
|
||||
|
||||
def sprint(string, *args, **kwargs):
|
||||
|
@ -47,6 +48,7 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
Telegram through Telethon, such as listing dialogs (open chats),
|
||||
talking to people, downloading media, and receiving updates.
|
||||
"""
|
||||
|
||||
def __init__(self, session_user_id, user_phone, api_id, api_hash,
|
||||
proxy=None):
|
||||
"""
|
||||
|
@ -84,9 +86,9 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
update_workers=1
|
||||
)
|
||||
|
||||
# Store all the found media in memory here,
|
||||
# so it can be downloaded if the user wants
|
||||
self.found_media = set()
|
||||
# Store {message.id: message} map here so that we can download
|
||||
# media known the message ID, for every message having media.
|
||||
self.found_media = {}
|
||||
|
||||
# Calling .connect() may return False, so you need to assert it's
|
||||
# True before continuing. Otherwise you may want to retry as done here.
|
||||
|
@ -138,15 +140,15 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
|
||||
# Entities represent the user, chat or channel
|
||||
# corresponding to the dialog on the same index.
|
||||
dialogs, entities = self.get_dialogs(limit=dialog_count)
|
||||
dialogs = self.get_dialogs(limit=dialog_count)
|
||||
|
||||
i = None
|
||||
while i is None:
|
||||
print_title('Dialogs window')
|
||||
|
||||
# Display them so the user can choose
|
||||
for i, entity in enumerate(entities, start=1):
|
||||
sprint('{}. {}'.format(i, get_display_name(entity)))
|
||||
for i, dialog in enumerate(dialogs, start=1):
|
||||
sprint('{}. {}'.format(i, get_display_name(dialog.entity)))
|
||||
|
||||
# Let the user decide who they want to talk to
|
||||
print()
|
||||
|
@ -177,19 +179,20 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
i = None
|
||||
|
||||
# Retrieve the selected user (or chat, or channel)
|
||||
entity = entities[i]
|
||||
entity = dialogs[i].entity
|
||||
|
||||
# Show some information
|
||||
print_title('Chat with "{}"'.format(get_display_name(entity)))
|
||||
print('Available commands:')
|
||||
print(' !q: Quits the current chat.')
|
||||
print(' !Q: Quits the current chat and exits.')
|
||||
print(' !h: prints the latest messages (message History).')
|
||||
print(' !up <path>: Uploads and sends the Photo from path.')
|
||||
print(' !uf <path>: Uploads and sends the File from path.')
|
||||
print(' !d <msg-id>: Deletes a message by its id')
|
||||
print(' !dm <msg-id>: Downloads the given message Media (if any).')
|
||||
print(' !q: Quits the current chat.')
|
||||
print(' !Q: Quits the current chat and exits.')
|
||||
print(' !h: prints the latest messages (message History).')
|
||||
print(' !up <path>: Uploads and sends the Photo from path.')
|
||||
print(' !uf <path>: Uploads and sends the File from path.')
|
||||
print(' !d <msg-id>: Deletes a message by its id')
|
||||
print(' !dm <msg-id>: Downloads the given message Media (if any).')
|
||||
print(' !dp: Downloads the current dialog Profile picture.')
|
||||
print(' !i: Prints information about this chat..')
|
||||
print()
|
||||
|
||||
# And start a while loop to chat
|
||||
|
@ -204,31 +207,23 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
# History
|
||||
elif msg == '!h':
|
||||
# First retrieve the messages and some information
|
||||
total_count, messages, senders = \
|
||||
self.get_message_history(entity, limit=10)
|
||||
messages = self.get_message_history(entity, limit=10)
|
||||
|
||||
# Iterate over all (in reverse order so the latest appear
|
||||
# the last in the console) and print them with format:
|
||||
# "[hh:mm] Sender: Message"
|
||||
for msg, sender in zip(
|
||||
reversed(messages), reversed(senders)):
|
||||
# Get the name of the sender if any
|
||||
if sender:
|
||||
name = getattr(sender, 'first_name', None)
|
||||
if not name:
|
||||
name = getattr(sender, 'title')
|
||||
if not name:
|
||||
name = '???'
|
||||
else:
|
||||
name = '???'
|
||||
for msg in reversed(messages):
|
||||
# Note that the .sender attribute is only there for
|
||||
# convenience, the API returns it differently. But
|
||||
# this shouldn't concern us. See the documentation
|
||||
# for .get_message_history() for more information.
|
||||
name = get_display_name(msg.sender)
|
||||
|
||||
# Format the message content
|
||||
if getattr(msg, 'media', None):
|
||||
self.found_media.add(msg)
|
||||
# The media may or may not have a caption
|
||||
caption = getattr(msg.media, 'caption', '')
|
||||
self.found_media[msg.id] = msg
|
||||
content = '<{}> {}'.format(
|
||||
type(msg.media).__name__, caption)
|
||||
type(msg.media).__name__, msg.message)
|
||||
|
||||
elif hasattr(msg, 'message'):
|
||||
content = msg.message
|
||||
|
@ -240,8 +235,7 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
|
||||
# And print it to the user
|
||||
sprint('[{}:{}] (ID={}) {}: {}'.format(
|
||||
msg.date.hour, msg.date.minute, msg.id, name,
|
||||
content))
|
||||
msg.date.hour, msg.date.minute, msg.id, name, content))
|
||||
|
||||
# Send photo
|
||||
elif msg.startswith('!up '):
|
||||
|
@ -257,8 +251,7 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
elif msg.startswith('!d '):
|
||||
# Slice the message to get message ID
|
||||
deleted_msg = self.delete_messages(entity, msg[len('!d '):])
|
||||
print('Deleted. {}'.format(deleted_msg))
|
||||
|
||||
print('Deleted {}'.format(deleted_msg))
|
||||
|
||||
# Download media
|
||||
elif msg.startswith('!dm '):
|
||||
|
@ -271,16 +264,19 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
os.makedirs('usermedia', exist_ok=True)
|
||||
output = self.download_profile_photo(entity, 'usermedia')
|
||||
if output:
|
||||
print(
|
||||
'Profile picture downloaded to {}'.format(output)
|
||||
)
|
||||
print('Profile picture downloaded to', output)
|
||||
else:
|
||||
print('No profile picture found for this user.')
|
||||
print('No profile picture found for this user!')
|
||||
|
||||
elif msg == '!i':
|
||||
attributes = list(entity.to_dict().items())
|
||||
pad = max(len(x) for x, _ in attributes)
|
||||
for name, val in attributes:
|
||||
print("{:<{width}} : {}".format(name, val, width=pad))
|
||||
|
||||
# Send chat message (if any)
|
||||
elif msg:
|
||||
self.send_message(
|
||||
entity, msg, link_preview=False)
|
||||
self.send_message(entity, msg, link_preview=False)
|
||||
|
||||
def send_photo(self, path, entity):
|
||||
"""Sends the file located at path to the desired entity as a photo"""
|
||||
|
@ -304,23 +300,20 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
downloads it.
|
||||
"""
|
||||
try:
|
||||
# The user may have entered a non-integer string!
|
||||
msg_media_id = int(media_id)
|
||||
msg = self.found_media[int(media_id)]
|
||||
except (ValueError, KeyError):
|
||||
# ValueError when parsing, KeyError when accessing dictionary
|
||||
print('Invalid media ID given or message not found!')
|
||||
return
|
||||
|
||||
# Search the message ID
|
||||
for msg in self.found_media:
|
||||
if msg.id == msg_media_id:
|
||||
print('Downloading media to usermedia/...')
|
||||
os.makedirs('usermedia', exist_ok=True)
|
||||
output = self.download_media(
|
||||
msg.media,
|
||||
file='usermedia/',
|
||||
progress_callback=self.download_progress_callback
|
||||
)
|
||||
print('Media downloaded to {}!'.format(output))
|
||||
|
||||
except ValueError:
|
||||
print('Invalid media ID given!')
|
||||
print('Downloading media to usermedia/...')
|
||||
os.makedirs('usermedia', exist_ok=True)
|
||||
output = self.download_media(
|
||||
msg.media,
|
||||
file='usermedia/',
|
||||
progress_callback=self.download_progress_callback
|
||||
)
|
||||
print('Media downloaded to {}!'.format(output))
|
||||
|
||||
@staticmethod
|
||||
def download_progress_callback(downloaded_bytes, total_bytes):
|
||||
|
@ -367,6 +360,5 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
else:
|
||||
who = self.get_entity(update.from_id)
|
||||
sprint('<< {} @ {} sent "{}"'.format(
|
||||
get_display_name(which), get_display_name(who),
|
||||
update.message
|
||||
get_display_name(which), get_display_name(who), update.message
|
||||
))
|
||||
|
|
|
@ -1,46 +1,36 @@
|
|||
#!/usr/bin/env python3
|
||||
# A simple script to print all updates received
|
||||
|
||||
from getpass import getpass
|
||||
from os import environ
|
||||
|
||||
# environ is used to get API information from environment variables
|
||||
# You could also use a config file, pass them as arguments,
|
||||
# or even hardcode them (not recommended)
|
||||
from telethon import TelegramClient
|
||||
from telethon.errors import SessionPasswordNeededError
|
||||
|
||||
|
||||
def main():
|
||||
session_name = environ.get('TG_SESSION', 'session')
|
||||
user_phone = environ['TG_PHONE']
|
||||
client = TelegramClient(session_name,
|
||||
int(environ['TG_API_ID']),
|
||||
environ['TG_API_HASH'],
|
||||
proxy=None,
|
||||
update_workers=4)
|
||||
update_workers=4,
|
||||
spawn_read_thread=False)
|
||||
|
||||
print('INFO: Connecting to Telegram Servers...', end='', flush=True)
|
||||
client.connect()
|
||||
print('Done!')
|
||||
|
||||
if not client.is_user_authorized():
|
||||
print('INFO: Unauthorized user')
|
||||
client.send_code_request(user_phone)
|
||||
code_ok = False
|
||||
while not code_ok:
|
||||
code = input('Enter the auth code: ')
|
||||
try:
|
||||
code_ok = client.sign_in(user_phone, code)
|
||||
except SessionPasswordNeededError:
|
||||
password = getpass('Two step verification enabled. Please enter your password: ')
|
||||
code_ok = client.sign_in(password=password)
|
||||
print('INFO: Client initialized succesfully!')
|
||||
if 'TG_PHONE' in environ:
|
||||
client.start(phone=environ['TG_PHONE'])
|
||||
else:
|
||||
client.start()
|
||||
|
||||
client.add_update_handler(update_handler)
|
||||
input('Press Enter to stop this!\n')
|
||||
print('(Press Ctrl+C to stop this)')
|
||||
client.idle()
|
||||
|
||||
|
||||
def update_handler(update):
|
||||
print(update)
|
||||
print('Press Enter to stop this!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -9,17 +9,12 @@ file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION.
|
|||
This script assumes that you have certain files on the working directory,
|
||||
such as "xfiles.m4a" or "anytime.png" for some of the automated replies.
|
||||
"""
|
||||
from getpass import getpass
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from os import environ
|
||||
|
||||
import re
|
||||
|
||||
from telethon import TelegramClient
|
||||
from telethon.errors import SessionPasswordNeededError
|
||||
from telethon.tl.types import UpdateNewChannelMessage, UpdateShortMessage, MessageService
|
||||
from telethon.tl.functions.messages import EditMessageRequest
|
||||
from telethon import TelegramClient, events, utils
|
||||
|
||||
"""Uncomment this for debugging
|
||||
import logging
|
||||
|
@ -35,103 +30,57 @@ REACTS = {'emacs': 'Needs more vim',
|
|||
recent_reacts = defaultdict(list)
|
||||
|
||||
|
||||
def update_handler(update):
|
||||
global recent_reacts
|
||||
try:
|
||||
msg = update.message
|
||||
except AttributeError:
|
||||
# print(update, 'did not have update.message')
|
||||
return
|
||||
if isinstance(msg, MessageService):
|
||||
print(msg, 'was service msg')
|
||||
return
|
||||
if __name__ == '__main__':
|
||||
# TG_API_ID and TG_API_HASH *must* exist or this won't run!
|
||||
session_name = environ.get('TG_SESSION', 'session')
|
||||
client = TelegramClient(
|
||||
session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'],
|
||||
spawn_read_thread=False, proxy=None, update_workers=4
|
||||
)
|
||||
|
||||
# React to messages in supergroups and PMs
|
||||
if isinstance(update, UpdateNewChannelMessage):
|
||||
words = re.split('\W+', msg.message)
|
||||
@client.on(events.NewMessage)
|
||||
def my_handler(event: events.NewMessage.Event):
|
||||
global recent_reacts
|
||||
|
||||
# This utils function gets the unique identifier from peers (to_id)
|
||||
to_id = utils.get_peer_id(event.message.to_id)
|
||||
|
||||
# Through event.raw_text we access the text of messages without format
|
||||
words = re.split('\W+', event.raw_text)
|
||||
|
||||
# Try to match some reaction
|
||||
for trigger, response in REACTS.items():
|
||||
if len(recent_reacts[msg.to_id.channel_id]) > 3:
|
||||
if len(recent_reacts[to_id]) > 3:
|
||||
# Silently ignore triggers if we've recently sent 3 reactions
|
||||
break
|
||||
|
||||
if trigger in words:
|
||||
# Remove recent replies older than 10 minutes
|
||||
recent_reacts[msg.to_id.channel_id] = [
|
||||
a for a in recent_reacts[msg.to_id.channel_id] if
|
||||
recent_reacts[to_id] = [
|
||||
a for a in recent_reacts[to_id] if
|
||||
datetime.now() - a < timedelta(minutes=10)
|
||||
]
|
||||
# Send a reaction
|
||||
client.send_message(msg.to_id, response, reply_to=msg.id)
|
||||
# Send a reaction as a reply (otherwise, event.respond())
|
||||
event.reply(response)
|
||||
# Add this reaction to the list of recent actions
|
||||
recent_reacts[msg.to_id.channel_id].append(datetime.now())
|
||||
recent_reacts[to_id].append(datetime.now())
|
||||
|
||||
if isinstance(update, UpdateShortMessage):
|
||||
words = re.split('\W+', msg)
|
||||
for trigger, response in REACTS.items():
|
||||
if len(recent_reacts[update.user_id]) > 3:
|
||||
# Silently ignore triggers if we've recently sent 3 reactions
|
||||
break
|
||||
# Automatically send relevant media when we say certain things
|
||||
# When invoking requests, get_input_entity needs to be called manually
|
||||
if event.out:
|
||||
if event.raw_text.lower() == 'x files theme':
|
||||
client.send_voice_note(event.message.to_id, 'xfiles.m4a',
|
||||
reply_to=event.message.id)
|
||||
if event.raw_text.lower() == 'anytime':
|
||||
client.send_file(event.message.to_id, 'anytime.png',
|
||||
reply_to=event.message.id)
|
||||
if '.shrug' in event.text:
|
||||
event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯'))
|
||||
|
||||
if trigger in words:
|
||||
# Send a reaction
|
||||
client.send_message(update.user_id, response, reply_to=update.id)
|
||||
# Add this reaction to the list of recent reactions
|
||||
recent_reacts[update.user_id].append(datetime.now())
|
||||
if 'TG_PHONE' in environ:
|
||||
client.start(phone=environ['TG_PHONE'])
|
||||
else:
|
||||
client.start()
|
||||
|
||||
# Automatically send relevant media when we say certain things
|
||||
# When invoking requests, get_input_entity needs to be called manually
|
||||
if isinstance(update, UpdateNewChannelMessage) and msg.out:
|
||||
if msg.message.lower() == 'x files theme':
|
||||
client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id)
|
||||
if msg.message.lower() == 'anytime':
|
||||
client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id)
|
||||
if '.shrug' in msg.message:
|
||||
client(EditMessageRequest(
|
||||
client.get_input_entity(msg.to_id), msg.id,
|
||||
message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯')
|
||||
))
|
||||
|
||||
if isinstance(update, UpdateShortMessage) and update.out:
|
||||
if msg.lower() == 'x files theme':
|
||||
client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id)
|
||||
if msg.lower() == 'anytime':
|
||||
client.send_file(update.user_id, 'anytime.png', reply_to=update.id)
|
||||
if '.shrug' in msg:
|
||||
client(EditMessageRequest(
|
||||
client.get_input_entity(update.user_id), update.id,
|
||||
message=msg.replace('.shrug', r'¯\_(ツ)_/¯')
|
||||
))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
session_name = environ.get('TG_SESSION', 'session')
|
||||
user_phone = environ['TG_PHONE']
|
||||
client = TelegramClient(
|
||||
session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'],
|
||||
proxy=None, update_workers=4
|
||||
)
|
||||
try:
|
||||
print('INFO: Connecting to Telegram Servers...', end='', flush=True)
|
||||
client.connect()
|
||||
print('Done!')
|
||||
|
||||
if not client.is_user_authorized():
|
||||
print('INFO: Unauthorized user')
|
||||
client.send_code_request(user_phone)
|
||||
code_ok = False
|
||||
while not code_ok:
|
||||
code = input('Enter the auth code: ')
|
||||
try:
|
||||
code_ok = client.sign_in(user_phone, code)
|
||||
except SessionPasswordNeededError:
|
||||
password = getpass('Two step verification enabled. '
|
||||
'Please enter your password: ')
|
||||
code_ok = client.sign_in(password=password)
|
||||
print('INFO: Client initialized successfully!')
|
||||
|
||||
client.add_update_handler(update_handler)
|
||||
input('Press Enter to stop this!\n')
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
client.disconnect()
|
||||
print('(Press Ctrl+C to stop this)')
|
||||
client.idle()
|
||||
|
|
|
@ -63,3 +63,4 @@ SESSION_REVOKED=The authorization has been invalidated, because of the user term
|
|||
USER_ALREADY_PARTICIPANT=The authenticated user is already a participant of the chat
|
||||
USER_DEACTIVATED=The user has been deleted/deactivated
|
||||
FLOOD_WAIT_X=A wait of {} seconds is required
|
||||
FLOOD_TEST_PHONE_WAIT_X=A wait of {} seconds is required in the test servers
|
||||
|
|
|
@ -11,6 +11,7 @@ known_base_classes = {
|
|||
401: 'UnauthorizedError',
|
||||
403: 'ForbiddenError',
|
||||
404: 'NotFoundError',
|
||||
406: 'AuthKeyError',
|
||||
420: 'FloodError',
|
||||
500: 'ServerError',
|
||||
}
|
||||
|
@ -26,7 +27,9 @@ known_codes = {
|
|||
|
||||
def fetch_errors(output, url=URL):
|
||||
print('Opening a connection to', url, '...')
|
||||
r = urllib.request.urlopen(url)
|
||||
r = urllib.request.urlopen(urllib.request.Request(
|
||||
url, headers={'User-Agent' : 'Mozilla/5.0'}
|
||||
))
|
||||
print('Checking response...')
|
||||
data = json.loads(
|
||||
r.read().decode(r.info().get_param('charset') or 'utf-8')
|
||||
|
@ -34,11 +37,11 @@ def fetch_errors(output, url=URL):
|
|||
if data.get('ok'):
|
||||
print('Response was okay, saving data')
|
||||
with open(output, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f)
|
||||
json.dump(data, f, sort_keys=True)
|
||||
return True
|
||||
else:
|
||||
print('The data received was not okay:')
|
||||
print(json.dumps(data, indent=4))
|
||||
print(json.dumps(data, indent=4, sort_keys=True))
|
||||
return False
|
||||
|
||||
|
||||
|
@ -66,7 +69,7 @@ def write_error(f, code, name, desc, capture_name):
|
|||
f.write(
|
||||
"self.{} = int(kwargs.get('capture', 0))\n ".format(capture_name)
|
||||
)
|
||||
f.write('super(Exception, self).__init__(self, {}'.format(repr(desc)))
|
||||
f.write('super(Exception, self).__init__({}'.format(repr(desc)))
|
||||
if capture_name:
|
||||
f.write('.format(self.{})'.format(capture_name))
|
||||
f.write(')\n')
|
||||
|
@ -79,7 +82,9 @@ def generate_code(output, json_file, errors_desc):
|
|||
errors = defaultdict(set)
|
||||
# PWRTelegram's API doesn't return all errors, which we do need here.
|
||||
# Add some special known-cases manually first.
|
||||
errors[420].add('FLOOD_WAIT_X')
|
||||
errors[420].update((
|
||||
'FLOOD_WAIT_X', 'FLOOD_TEST_PHONE_WAIT_X'
|
||||
))
|
||||
errors[401].update((
|
||||
'AUTH_KEY_INVALID', 'SESSION_EXPIRED', 'SESSION_REVOKED'
|
||||
))
|
||||
|
@ -118,6 +123,7 @@ def generate_code(output, json_file, errors_desc):
|
|||
# Names for the captures, or 'x' if unknown
|
||||
capture_names = {
|
||||
'FloodWaitError': 'seconds',
|
||||
'FloodTestPhoneWaitError': 'seconds',
|
||||
'FileMigrateError': 'new_dc',
|
||||
'NetworkMigrateError': 'new_dc',
|
||||
'PhoneMigrateError': 'new_dc',
|
||||
|
@ -161,3 +167,11 @@ def generate_code(output, json_file, errors_desc):
|
|||
for pattern, name in patterns:
|
||||
f.write(' {}: {},\n'.format(repr(pattern), name))
|
||||
f.write('}\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if input('generate (y/n)?: ').lower() == 'y':
|
||||
generate_code('../telethon/errors/rpc_error_list.py',
|
||||
'errors.json', 'error_descriptions')
|
||||
elif input('fetch (y/n)?: ').lower() == 'y':
|
||||
fetch_errors('errors.json')
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -16,7 +16,7 @@ class SourceBuilder:
|
|||
"""
|
||||
self.write(' ' * (self.current_indent * self.indent_size))
|
||||
|
||||
def write(self, string):
|
||||
def write(self, string, *args, **kwargs):
|
||||
"""Writes a string into the source code,
|
||||
applying indentation if required
|
||||
"""
|
||||
|
@ -26,13 +26,16 @@ class SourceBuilder:
|
|||
if string.strip():
|
||||
self.indent()
|
||||
|
||||
self.out_stream.write(string)
|
||||
if args or kwargs:
|
||||
self.out_stream.write(string.format(*args, **kwargs))
|
||||
else:
|
||||
self.out_stream.write(string)
|
||||
|
||||
def writeln(self, string=''):
|
||||
def writeln(self, string='', *args, **kwargs):
|
||||
"""Writes a string into the source code _and_ appends a new line,
|
||||
applying indentation if required
|
||||
"""
|
||||
self.write(string + '\n')
|
||||
self.write(string + '\n', *args, **kwargs)
|
||||
self.on_new_line = True
|
||||
|
||||
# If we're writing a block, increment indent for the next time
|
||||
|
|
|
@ -254,7 +254,7 @@ class TLArg:
|
|||
|
||||
self.generic_definition = generic_definition
|
||||
|
||||
def type_hint(self):
|
||||
def doc_type_hint(self):
|
||||
result = {
|
||||
'int': 'int',
|
||||
'long': 'int',
|
||||
|
@ -264,7 +264,7 @@ class TLArg:
|
|||
'date': 'datetime.datetime | None', # None date = 0 timestamp
|
||||
'bytes': 'bytes',
|
||||
'true': 'bool',
|
||||
}.get(self.type, 'TLObject')
|
||||
}.get(self.type, self.type)
|
||||
if self.is_vector:
|
||||
result = 'list[{}]'.format(result)
|
||||
if self.is_flag and self.type != 'date':
|
||||
|
@ -272,6 +272,27 @@ class TLArg:
|
|||
|
||||
return result
|
||||
|
||||
def python_type_hint(self):
|
||||
type = self.type
|
||||
if '.' in type:
|
||||
type = type.split('.')[1]
|
||||
result = {
|
||||
'int': 'int',
|
||||
'long': 'int',
|
||||
'int128': 'int',
|
||||
'int256': 'int',
|
||||
'string': 'str',
|
||||
'date': 'Optional[datetime]', # None date = 0 timestamp
|
||||
'bytes': 'bytes',
|
||||
'true': 'bool',
|
||||
}.get(type, "Type{}".format(type))
|
||||
if self.is_vector:
|
||||
result = 'List[{}]'.format(result)
|
||||
if self.is_flag and type != 'date':
|
||||
result = 'Optional[{}]'.format(result)
|
||||
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
# Find the real type representation by updating it as required
|
||||
real_type = self.type
|
||||
|
|
|
@ -53,7 +53,10 @@ destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes;
|
|||
|
||||
---functions---
|
||||
|
||||
// Deprecated since somewhere around February of 2018
|
||||
// See https://core.telegram.org/mtproto/auth_key
|
||||
req_pq#60469778 nonce:int128 = ResPQ;
|
||||
req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
|
||||
|
||||
req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params;
|
||||
|
||||
|
@ -155,22 +158,20 @@ inputFile#f52ff27f id:long parts:int name:string md5_checksum:string = InputFile
|
|||
inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile;
|
||||
|
||||
inputMediaEmpty#9664f57f = InputMedia;
|
||||
inputMediaUploadedPhoto#2f37e231 flags:# file:InputFile caption:string stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||
inputMediaPhoto#81fa373a flags:# id:InputPhoto caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaUploadedPhoto#1e287d04 flags:# file:InputFile stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||
inputMediaPhoto#b3ba0635 flags:# id:InputPhoto ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
|
||||
inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia;
|
||||
inputMediaUploadedDocument#e39621fd flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||
inputMediaDocument#5acb668e flags:# id:InputDocument caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||
inputMediaDocument#23ab23d2 flags:# id:InputDocument ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia;
|
||||
inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
|
||||
inputMediaPhotoExternal#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
|
||||
inputMediaInvoice#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia;
|
||||
inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia;
|
||||
inputMediaGeoLive#7b1a118f geo_point:InputGeoPoint period:int = InputMedia;
|
||||
|
||||
inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia;
|
||||
|
||||
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
|
||||
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
|
||||
inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto;
|
||||
|
@ -242,11 +243,11 @@ message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:fl
|
|||
messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message;
|
||||
|
||||
messageMediaEmpty#3ded6320 = MessageMedia;
|
||||
messageMediaPhoto#b5223b0f flags:# photo:flags.0?Photo caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia;
|
||||
messageMediaPhoto#695150d7 flags:# photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia;
|
||||
messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia;
|
||||
messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:string user_id:int = MessageMedia;
|
||||
messageMediaUnsupported#9f84f49e = MessageMedia;
|
||||
messageMediaDocument#7c4414d3 flags:# document:flags.0?Document caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia;
|
||||
messageMediaDocument#9cb070d7 flags:# document:flags.0?Document ttl_seconds:flags.2?int = MessageMedia;
|
||||
messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia;
|
||||
messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia;
|
||||
messageMediaGame#fdb19008 game:Game = MessageMedia;
|
||||
|
@ -345,6 +346,7 @@ messages.dialogsSlice#71e094f3 count:int dialogs:Vector<Dialog> messages:Vector<
|
|||
messages.messages#8c718e87 messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
|
||||
messages.messagesSlice#b446ae3 count:int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
|
||||
messages.channelMessages#99262e37 flags:# pts:int count:int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
|
||||
messages.messagesNotModified#74535f21 count:int = messages.Messages;
|
||||
|
||||
messages.chats#64ff9fd5 chats:Vector<Chat> = messages.Chats;
|
||||
messages.chatsSlice#9cd81144 count:int chats:Vector<Chat> = messages.Chats;
|
||||
|
@ -357,7 +359,6 @@ inputMessagesFilterEmpty#57e2f66c = MessagesFilter;
|
|||
inputMessagesFilterPhotos#9609a51c = MessagesFilter;
|
||||
inputMessagesFilterVideo#9fc00e65 = MessagesFilter;
|
||||
inputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter;
|
||||
inputMessagesFilterPhotoVideoDocuments#d95e73bb = MessagesFilter;
|
||||
inputMessagesFilterDocument#9eddf188 = MessagesFilter;
|
||||
inputMessagesFilterUrl#7ef0dd87 = MessagesFilter;
|
||||
inputMessagesFilterGif#ffc86587 = MessagesFilter;
|
||||
|
@ -368,8 +369,8 @@ inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFil
|
|||
inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter;
|
||||
inputMessagesFilterRoundVideo#b549da53 = MessagesFilter;
|
||||
inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter;
|
||||
inputMessagesFilterContacts#e062db83 = MessagesFilter;
|
||||
inputMessagesFilterGeo#e7026d0d = MessagesFilter;
|
||||
inputMessagesFilterContacts#e062db83 = MessagesFilter;
|
||||
|
||||
updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update;
|
||||
updateMessageID#4e90bfd6 id:int random_id:long = Update;
|
||||
|
@ -463,7 +464,7 @@ upload.fileCdnRedirect#ea52fe5a dc_id:int file_token:bytes encryption_key:bytes
|
|||
|
||||
dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int = DcOption;
|
||||
|
||||
config#9c840964 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<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 stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector<DisabledFeature> = Config;
|
||||
config#9c840964 flags:# phonecalls_enabled:flags.1?true default_p2p_contacts:flags.3?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<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 stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector<DisabledFeature> = Config;
|
||||
|
||||
nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;
|
||||
|
||||
|
@ -524,7 +525,7 @@ sendMessageGamePlayAction#dd6a8f48 = SendMessageAction;
|
|||
sendMessageRecordRoundAction#88f27fbc = SendMessageAction;
|
||||
sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction;
|
||||
|
||||
contacts.found#1aa1f784 results:Vector<Peer> chats:Vector<Chat> users:Vector<User> = contacts.Found;
|
||||
contacts.found#b3134d9d my_results:Vector<Peer> results:Vector<Peer> chats:Vector<Chat> users:Vector<User> = contacts.Found;
|
||||
|
||||
inputPrivacyKeyStatusTimestamp#4f96cb18 = InputPrivacyKey;
|
||||
inputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey;
|
||||
|
@ -555,7 +556,7 @@ accountDaysTTL#b8d0afdf days:int = AccountDaysTTL;
|
|||
documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute;
|
||||
documentAttributeAnimated#11b58939 = DocumentAttribute;
|
||||
documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute;
|
||||
documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true duration:int w:int h:int = DocumentAttribute;
|
||||
documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true supports_streaming:flags.1?true duration:int w:int h:int = DocumentAttribute;
|
||||
documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute;
|
||||
documentAttributeFilename#15590068 file_name:string = DocumentAttribute;
|
||||
documentAttributeHasStickers#9801d2f7 = DocumentAttribute;
|
||||
|
@ -687,7 +688,7 @@ messages.foundGifs#450a1c0a next_offset:int results:Vector<FoundGif> = messages.
|
|||
messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs;
|
||||
messages.savedGifs#2e0709a5 hash:int gifs:Vector<Document> = messages.SavedGifs;
|
||||
|
||||
inputBotInlineMessageMediaAuto#292fed13 flags:# caption:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
inputBotInlineMessageMediaAuto#3380c786 flags:# message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
inputBotInlineMessageMediaGeo#c1b15d65 flags:# geo_point:InputGeoPoint period:int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
inputBotInlineMessageMediaVenue#aaafadc8 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
|
@ -699,7 +700,7 @@ inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_m
|
|||
inputBotInlineResultDocument#fff8fdc4 flags:# id:string type:string title:flags.1?string description:flags.2?string document:InputDocument send_message:InputBotInlineMessage = InputBotInlineResult;
|
||||
inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:InputBotInlineMessage = InputBotInlineResult;
|
||||
|
||||
botInlineMessageMediaAuto#a74b15b flags:# caption:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
|
||||
botInlineMessageMediaAuto#764cf810 flags:# message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
|
||||
botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
|
||||
botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
|
||||
botInlineMessageMediaVenue#4366232e flags:# geo:GeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
|
||||
|
@ -710,7 +711,7 @@ botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo
|
|||
|
||||
messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector<BotInlineResult> cache_time:int users:Vector<User> = messages.BotResults;
|
||||
|
||||
exportedMessageLink#1f486803 link:string = ExportedMessageLink;
|
||||
exportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink;
|
||||
|
||||
messageFwdHeader#559ebe6d flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int = MessageFwdHeader;
|
||||
|
||||
|
@ -723,7 +724,7 @@ auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType;
|
|||
auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType;
|
||||
auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType;
|
||||
|
||||
messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer;
|
||||
messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer;
|
||||
|
||||
messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData;
|
||||
|
||||
|
@ -825,7 +826,7 @@ dataJSON#7d748d04 data:string = DataJSON;
|
|||
|
||||
labeledPrice#cb296bf8 label:string amount:long = LabeledPrice;
|
||||
|
||||
invoice#c30aa358 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true currency:string prices:Vector<LabeledPrice> = Invoice;
|
||||
invoice#c30aa358 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true currency:string prices:Vector<LabeledPrice> = Invoice;
|
||||
|
||||
paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge;
|
||||
|
||||
|
@ -856,6 +857,8 @@ payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_inf
|
|||
|
||||
inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials;
|
||||
inputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials;
|
||||
inputPaymentCredentialsApplePay#aa1c39f payment_data:DataJSON = InputPaymentCredentials;
|
||||
inputPaymentCredentialsAndroidPay#ca05d50e payment_token:DataJSON google_transaction_id:string = InputPaymentCredentials;
|
||||
|
||||
account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword;
|
||||
|
||||
|
@ -927,13 +930,23 @@ cdnFileHash#77eec38f offset:int limit:int hash:bytes = CdnFileHash;
|
|||
messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers;
|
||||
messages.favedStickers#f37f2f16 hash:int packs:Vector<StickerPack> stickers:Vector<Document> = messages.FavedStickers;
|
||||
|
||||
help.recentMeUrls#e0310d7 urls:Vector<RecentMeUrl> chats:Vector<Chat> users:Vector<User> = help.RecentMeUrls;
|
||||
|
||||
recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl;
|
||||
recentMeUrlUser#8dbc3336 url:string user_id:int = RecentMeUrl;
|
||||
recentMeUrlChat#a01b22f9 url:string chat_id:int = RecentMeUrl;
|
||||
recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl;
|
||||
recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl;
|
||||
recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl;
|
||||
recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl;
|
||||
|
||||
help.recentMeUrls#e0310d7 urls:Vector<RecentMeUrl> chats:Vector<Chat> users:Vector<User> = help.RecentMeUrls;
|
||||
|
||||
inputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector<MessageEntity> = InputSingleMedia;
|
||||
|
||||
webAuthorization#cac943f2 hash:long bot_id:int domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization;
|
||||
|
||||
account.webAuthorizations#ed56c9fc authorizations:Vector<WebAuthorization> users:Vector<User> = account.WebAuthorizations;
|
||||
|
||||
inputMessageID#a676a322 id:int = InputMessage;
|
||||
inputMessageReplyTo#bad88395 id:int = InputMessage;
|
||||
inputMessagePinned#86872538 = InputMessage;
|
||||
|
||||
---functions---
|
||||
|
||||
|
@ -961,8 +974,8 @@ auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentC
|
|||
auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool;
|
||||
auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector<long> = Bool;
|
||||
|
||||
account.registerDevice#637ea878 token_type:int token:string = Bool;
|
||||
account.unregisterDevice#65c55b40 token_type:int token:string = Bool;
|
||||
account.registerDevice#1389cc token_type:int token:string app_sandbox:Bool other_uids:Vector<int> = Bool;
|
||||
account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector<int> = Bool;
|
||||
account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool;
|
||||
account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings;
|
||||
account.resetNotifySettings#db7e1747 = Bool;
|
||||
|
@ -988,6 +1001,9 @@ account.updatePasswordSettings#fa7c4b86 current_password_hash:bytes new_settings
|
|||
account.sendConfirmPhoneCode#1516d7bd flags:# allow_flashcall:flags.0?true hash:string current_number:flags.0?Bool = auth.SentCode;
|
||||
account.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool;
|
||||
account.getTmpPassword#4a82327e password_hash:bytes period:int = account.TmpPassword;
|
||||
account.getWebAuthorizations#182e6d6f = account.WebAuthorizations;
|
||||
account.resetWebAuthorization#2d01b9ef hash:long = Bool;
|
||||
account.resetWebAuthorizations#682d2594 = Bool;
|
||||
|
||||
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
|
||||
users.getFullUser#ca30a5b1 id:InputUser = UserFull;
|
||||
|
@ -1008,9 +1024,9 @@ contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.
|
|||
contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;
|
||||
contacts.resetSaved#879537f1 = Bool;
|
||||
|
||||
messages.getMessages#4222fa74 id:Vector<int> = messages.Messages;
|
||||
messages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages;
|
||||
messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs;
|
||||
messages.getHistory#afa92846 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
||||
messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;
|
||||
messages.search#39e9ea0 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
||||
messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages;
|
||||
messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true peer:InputPeer max_id:int = messages.AffectedHistory;
|
||||
|
@ -1018,7 +1034,7 @@ messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = me
|
|||
messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>;
|
||||
messages.setTyping#a3825e50 peer:InputPeer action:SendMessageAction = Bool;
|
||||
messages.sendMessage#fa88427a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Updates;
|
||||
messages.sendMedia#c8f16791 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia random_id:long reply_markup:flags.2?ReplyMarkup = Updates;
|
||||
messages.sendMedia#b8d1262b flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Updates;
|
||||
messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true grouped:flags.9?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer = Updates;
|
||||
messages.reportSpam#cf1592db peer:InputPeer = Bool;
|
||||
messages.hideReportSpam#a8f1709b peer:InputPeer = Bool;
|
||||
|
@ -1030,7 +1046,6 @@ messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates;
|
|||
messages.addChatUser#f9a0aa09 chat_id:int user_id:InputUser fwd_limit:int = Updates;
|
||||
messages.deleteChatUser#e0611f16 chat_id:int user_id:InputUser = Updates;
|
||||
messages.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
|
||||
messages.forwardMessage#33963bf9 peer:InputPeer id:int random_id:long = Updates;
|
||||
messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;
|
||||
messages.requestEncryption#f64daf43 user_id:InputUser random_id:int g_a:bytes = EncryptedChat;
|
||||
messages.acceptEncryption#3dbc0415 peer:InputEncryptedChat g_b:bytes key_fingerprint:long = EncryptedChat;
|
||||
|
@ -1043,8 +1058,9 @@ messages.sendEncryptedService#32d439a4 peer:InputEncryptedChat random_id:long da
|
|||
messages.receivedQueue#55a5bb66 max_qts:int = Vector<long>;
|
||||
messages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool;
|
||||
messages.readMessageContents#36a73f77 id:Vector<int> = messages.AffectedMessages;
|
||||
messages.getStickers#ae22e045 emoticon:string hash:string = messages.Stickers;
|
||||
messages.getAllStickers#1c9618b1 hash:int = messages.AllStickers;
|
||||
messages.getWebPagePreview#25223e24 message:string = MessageMedia;
|
||||
messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector<MessageEntity> = MessageMedia;
|
||||
messages.exportChatInvite#7d885289 chat_id:int = ExportedChatInvite;
|
||||
messages.checkChatInvite#3eadb1bb hash:string = ChatInvite;
|
||||
messages.importChatInvite#6c50051c hash:string = Updates;
|
||||
|
@ -1067,7 +1083,7 @@ messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags
|
|||
messages.sendInlineBotResult#b16e06fe flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates;
|
||||
messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData;
|
||||
messages.editMessage#5d1b8dd flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> geo_point:flags.13?InputGeoPoint = Updates;
|
||||
messages.editInlineBotMessage#130c2c85 flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Bool;
|
||||
messages.editInlineBotMessage#b0e08243 flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> geo_point:flags.13?InputGeoPoint = Bool;
|
||||
messages.getBotCallbackAnswer#810a9fec flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes = messages.BotCallbackAnswer;
|
||||
messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool;
|
||||
messages.getPeerDialogs#2d9776b9 peers:Vector<InputPeer> = messages.PeerDialogs;
|
||||
|
@ -1098,9 +1114,10 @@ messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int
|
|||
messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers;
|
||||
messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool;
|
||||
messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
||||
messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages;
|
||||
messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory;
|
||||
messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages;
|
||||
messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector<InputSingleMedia> = Updates;
|
||||
messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile;
|
||||
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||
|
@ -1135,7 +1152,7 @@ channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
|
|||
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
|
||||
channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory;
|
||||
channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector<int> = Bool;
|
||||
channels.getMessages#93d7b347 channel:InputChannel id:Vector<int> = messages.Messages;
|
||||
channels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages;
|
||||
channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants;
|
||||
channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant;
|
||||
channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;
|
||||
|
@ -1153,7 +1170,7 @@ channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector<InputUser> =
|
|||
channels.exportInvite#c7560885 channel:InputChannel = ExportedChatInvite;
|
||||
channels.deleteChannel#c0111fe3 channel:InputChannel = Updates;
|
||||
channels.toggleInvites#49609307 channel:InputChannel enabled:Bool = Updates;
|
||||
channels.exportMessageLink#c846d22d channel:InputChannel id:int = ExportedMessageLink;
|
||||
channels.exportMessageLink#ceb77163 channel:InputChannel id:int grouped:Bool = ExportedMessageLink;
|
||||
channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates;
|
||||
channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates;
|
||||
channels.getAdminedPublicChannels#8d8d82d7 = messages.Chats;
|
||||
|
@ -1193,4 +1210,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector<string> = Vector<LangP
|
|||
langpack.getDifference#b2e4d7d from_version:int = LangPackDifference;
|
||||
langpack.getLanguages#800fd57d = Vector<LangPackLanguage>;
|
||||
|
||||
// LAYER 73
|
||||
// LAYER 75
|
||||
|
|
|
@ -10,14 +10,25 @@ AUTO_GEN_NOTICE = \
|
|||
'"""File generated by TLObjects\' generator. All changes will be ERASED"""'
|
||||
|
||||
|
||||
AUTO_CASTS = {
|
||||
'InputPeer': 'utils.get_input_peer(client.get_input_entity({}))',
|
||||
'InputChannel': 'utils.get_input_channel(client.get_input_entity({}))',
|
||||
'InputUser': 'utils.get_input_user(client.get_input_entity({}))',
|
||||
'InputMedia': 'utils.get_input_media({})',
|
||||
'InputPhoto': 'utils.get_input_photo({})'
|
||||
}
|
||||
|
||||
|
||||
class TLGenerator:
|
||||
def __init__(self, output_dir):
|
||||
self.output_dir = output_dir
|
||||
|
||||
def _get_file(self, *paths):
|
||||
"""Wrapper around ``os.path.join()`` with output as first path."""
|
||||
return os.path.join(self.output_dir, *paths)
|
||||
|
||||
def _rm_if_exists(self, filename):
|
||||
"""Recursively deletes the given filename if it exists."""
|
||||
file = self._get_file(filename)
|
||||
if os.path.exists(file):
|
||||
if os.path.isdir(file):
|
||||
|
@ -26,19 +37,21 @@ class TLGenerator:
|
|||
os.remove(file)
|
||||
|
||||
def tlobjects_exist(self):
|
||||
"""Determines whether the TLObjects were previously
|
||||
generated (hence exist) or not
|
||||
"""
|
||||
Determines whether the TLObjects were previously
|
||||
generated (hence exist) or not.
|
||||
"""
|
||||
return os.path.isfile(self._get_file('all_tlobjects.py'))
|
||||
|
||||
def clean_tlobjects(self):
|
||||
"""Cleans the automatically generated TLObjects from disk"""
|
||||
"""Cleans the automatically generated TLObjects from disk."""
|
||||
for name in ('functions', 'types', 'all_tlobjects.py'):
|
||||
self._rm_if_exists(name)
|
||||
|
||||
def generate_tlobjects(self, scheme_file, import_depth):
|
||||
"""Generates all the TLObjects from scheme.tl to
|
||||
tl/functions and tl/types
|
||||
"""
|
||||
Generates all the TLObjects from the ``scheme_file`` to
|
||||
``tl/functions`` and ``tl/types``.
|
||||
"""
|
||||
|
||||
# First ensure that the required parent directories exist
|
||||
|
@ -76,42 +89,33 @@ class TLGenerator:
|
|||
# Step 4: Once all the objects have been generated,
|
||||
# we can now group them in a single file
|
||||
filename = os.path.join(self._get_file('all_tlobjects.py'))
|
||||
with open(filename, 'w', encoding='utf-8') as file:
|
||||
with SourceBuilder(file) as builder:
|
||||
builder.writeln(AUTO_GEN_NOTICE)
|
||||
builder.writeln()
|
||||
with open(filename, 'w', encoding='utf-8') as file,\
|
||||
SourceBuilder(file) as builder:
|
||||
builder.writeln(AUTO_GEN_NOTICE)
|
||||
builder.writeln()
|
||||
|
||||
builder.writeln('from . import types, functions')
|
||||
builder.writeln()
|
||||
builder.writeln('from . import types, functions')
|
||||
builder.writeln()
|
||||
|
||||
# Create a constant variable to indicate which layer this is
|
||||
builder.writeln('LAYER = {}'.format(
|
||||
TLParser.find_layer(scheme_file))
|
||||
)
|
||||
builder.writeln()
|
||||
# Create a constant variable to indicate which layer this is
|
||||
builder.writeln('LAYER = {}', TLParser.find_layer(scheme_file))
|
||||
builder.writeln()
|
||||
|
||||
# Then create the dictionary containing constructor_id: class
|
||||
builder.writeln('tlobjects = {')
|
||||
builder.current_indent += 1
|
||||
# Then create the dictionary containing constructor_id: class
|
||||
builder.writeln('tlobjects = {')
|
||||
builder.current_indent += 1
|
||||
|
||||
# Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)
|
||||
for tlobject in tlobjects:
|
||||
constructor = hex(tlobject.id)
|
||||
if len(constructor) != 10:
|
||||
# Make it a nice length 10 so it fits well
|
||||
constructor = '0x' + constructor[2:].zfill(8)
|
||||
# Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)
|
||||
for tlobject in tlobjects:
|
||||
builder.write('{:#010x}: ', tlobject.id)
|
||||
builder.write('functions' if tlobject.is_function else 'types')
|
||||
if tlobject.namespace:
|
||||
builder.write('.' + tlobject.namespace)
|
||||
|
||||
builder.write('{}: '.format(constructor))
|
||||
builder.write(
|
||||
'functions' if tlobject.is_function else 'types')
|
||||
builder.writeln('.{},', tlobject.class_name())
|
||||
|
||||
if tlobject.namespace:
|
||||
builder.write('.' + tlobject.namespace)
|
||||
|
||||
builder.writeln('.{},'.format(tlobject.class_name()))
|
||||
|
||||
builder.current_indent -= 1
|
||||
builder.writeln('}')
|
||||
builder.current_indent -= 1
|
||||
builder.writeln('}')
|
||||
|
||||
@staticmethod
|
||||
def _write_init_py(out_dir, depth, namespace_tlobjects, type_constructors):
|
||||
|
@ -127,24 +131,17 @@ class TLGenerator:
|
|||
# so they all can be serialized and sent, however, only the
|
||||
# functions are "content_related".
|
||||
builder.writeln(
|
||||
'from {}.tl.tlobject import TLObject'.format('.' * depth)
|
||||
'from {}.tl.tlobject import TLObject', '.' * depth
|
||||
)
|
||||
builder.writeln('from typing import Optional, List, '
|
||||
'Union, TYPE_CHECKING')
|
||||
|
||||
# Add the relative imports to the namespaces,
|
||||
# unless we already are in a namespace.
|
||||
if not ns:
|
||||
builder.writeln('from . import {}'.format(', '.join(
|
||||
builder.writeln('from . import {}', ', '.join(
|
||||
x for x in namespace_tlobjects.keys() if x
|
||||
)))
|
||||
|
||||
# Import 'get_input_*' utils
|
||||
# TODO Support them on types too
|
||||
if 'functions' in out_dir:
|
||||
builder.writeln(
|
||||
'from {}.utils import get_input_peer, '
|
||||
'get_input_channel, get_input_user, '
|
||||
'get_input_media, get_input_photo'.format('.' * depth)
|
||||
)
|
||||
))
|
||||
|
||||
# Import 'os' for those needing access to 'os.urandom()'
|
||||
# Currently only 'random_id' needs 'os' to be imported,
|
||||
|
@ -154,31 +151,98 @@ class TLGenerator:
|
|||
# Import struct for the .__bytes__(self) serialization
|
||||
builder.writeln('import struct')
|
||||
|
||||
tlobjects.sort(key=lambda x: x.name)
|
||||
|
||||
type_names = set()
|
||||
type_defs = []
|
||||
|
||||
# Find all the types in this file and generate type definitions
|
||||
# based on the types. The type definitions are written to the
|
||||
# file at the end.
|
||||
for t in tlobjects:
|
||||
if not t.is_function:
|
||||
type_name = t.result
|
||||
if '.' in type_name:
|
||||
type_name = type_name[type_name.rindex('.'):]
|
||||
if type_name in type_names:
|
||||
continue
|
||||
type_names.add(type_name)
|
||||
constructors = type_constructors[type_name]
|
||||
if not constructors:
|
||||
pass
|
||||
elif len(constructors) == 1:
|
||||
type_defs.append('Type{} = {}'.format(
|
||||
type_name, constructors[0].class_name()))
|
||||
else:
|
||||
type_defs.append('Type{} = Union[{}]'.format(
|
||||
type_name, ','.join(c.class_name()
|
||||
for c in constructors)))
|
||||
|
||||
imports = {}
|
||||
primitives = ('int', 'long', 'int128', 'int256', 'string',
|
||||
'date', 'bytes', 'true')
|
||||
# Find all the types in other files that are used in this file
|
||||
# and generate the information required to import those types.
|
||||
for t in tlobjects:
|
||||
for arg in t.args:
|
||||
name = arg.type
|
||||
if not name or name in primitives:
|
||||
continue
|
||||
|
||||
import_space = '{}.tl.types'.format('.' * depth)
|
||||
if '.' in name:
|
||||
namespace = name.split('.')[0]
|
||||
name = name.split('.')[1]
|
||||
import_space += '.{}'.format(namespace)
|
||||
|
||||
if name not in type_names:
|
||||
type_names.add(name)
|
||||
if name == 'date':
|
||||
imports['datetime'] = ['datetime']
|
||||
continue
|
||||
elif import_space not in imports:
|
||||
imports[import_space] = set()
|
||||
imports[import_space].add('Type{}'.format(name))
|
||||
|
||||
# Add imports required for type checking
|
||||
if imports:
|
||||
builder.writeln('if TYPE_CHECKING:')
|
||||
for namespace, names in imports.items():
|
||||
builder.writeln('from {} import {}',
|
||||
namespace, ', '.join(names))
|
||||
|
||||
builder.end_block()
|
||||
|
||||
# Generate the class for every TLObject
|
||||
for t in sorted(tlobjects, key=lambda x: x.name):
|
||||
for t in tlobjects:
|
||||
TLGenerator._write_source_code(
|
||||
t, builder, depth, type_constructors
|
||||
)
|
||||
builder.current_indent = 0
|
||||
|
||||
# Write the type definitions generated earlier.
|
||||
builder.writeln('')
|
||||
for line in type_defs:
|
||||
builder.writeln(line)
|
||||
|
||||
@staticmethod
|
||||
def _write_source_code(tlobject, builder, depth, type_constructors):
|
||||
"""Writes the source code corresponding to the given TLObject
|
||||
by making use of the 'builder' SourceBuilder.
|
||||
"""
|
||||
Writes the source code corresponding to the given TLObject
|
||||
by making use of the ``builder`` `SourceBuilder`.
|
||||
|
||||
Additional information such as file path depth and
|
||||
the Type: [Constructors] must be given for proper
|
||||
importing and documentation strings.
|
||||
Additional information such as file path depth and
|
||||
the ``Type: [Constructors]`` must be given for proper
|
||||
importing and documentation strings.
|
||||
"""
|
||||
builder.writeln()
|
||||
builder.writeln()
|
||||
builder.writeln('class {}(TLObject):'.format(tlobject.class_name()))
|
||||
builder.writeln('class {}(TLObject):', tlobject.class_name())
|
||||
|
||||
# Class-level variable to store its Telegram's constructor ID
|
||||
builder.writeln('CONSTRUCTOR_ID = {}'.format(hex(tlobject.id)))
|
||||
builder.writeln('SUBCLASS_OF_ID = {}'.format(
|
||||
hex(crc32(tlobject.result.encode('ascii'))))
|
||||
)
|
||||
builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id)
|
||||
builder.writeln('SUBCLASS_OF_ID = {:#x}',
|
||||
crc32(tlobject.result.encode('ascii')))
|
||||
builder.writeln()
|
||||
|
||||
# Flag arguments must go last
|
||||
|
@ -196,9 +260,7 @@ class TLGenerator:
|
|||
|
||||
# Write the __init__ function
|
||||
if args:
|
||||
builder.writeln(
|
||||
'def __init__(self, {}):'.format(', '.join(args))
|
||||
)
|
||||
builder.writeln('def __init__(self, {}):', ', '.join(args))
|
||||
else:
|
||||
builder.writeln('def __init__(self):')
|
||||
|
||||
|
@ -217,30 +279,27 @@ class TLGenerator:
|
|||
builder.writeln('"""')
|
||||
for arg in args:
|
||||
if not arg.flag_indicator:
|
||||
builder.writeln(':param {} {}:'.format(
|
||||
arg.type_hint(), arg.name
|
||||
))
|
||||
builder.writeln(':param {} {}:',
|
||||
arg.doc_type_hint(), arg.name)
|
||||
builder.current_indent -= 1 # It will auto-indent (':')
|
||||
|
||||
# We also want to know what type this request returns
|
||||
# or to which type this constructor belongs to
|
||||
builder.writeln()
|
||||
if tlobject.is_function:
|
||||
builder.write(':returns {}: '.format(tlobject.result))
|
||||
builder.write(':returns {}: ', tlobject.result)
|
||||
else:
|
||||
builder.write('Constructor for {}: '.format(tlobject.result))
|
||||
builder.write('Constructor for {}: ', tlobject.result)
|
||||
|
||||
constructors = type_constructors[tlobject.result]
|
||||
if not constructors:
|
||||
builder.writeln('This type has no constructors.')
|
||||
elif len(constructors) == 1:
|
||||
builder.writeln('Instance of {}.'.format(
|
||||
constructors[0].class_name()
|
||||
))
|
||||
builder.writeln('Instance of {}.',
|
||||
constructors[0].class_name())
|
||||
else:
|
||||
builder.writeln('Instance of either {}.'.format(
|
||||
', '.join(c.class_name() for c in constructors)
|
||||
))
|
||||
builder.writeln('Instance of either {}.', ', '.join(
|
||||
c.class_name() for c in constructors))
|
||||
|
||||
builder.writeln('"""')
|
||||
|
||||
|
@ -257,43 +316,78 @@ class TLGenerator:
|
|||
builder.writeln()
|
||||
|
||||
for arg in args:
|
||||
TLGenerator._write_self_assigns(builder, tlobject, arg, args)
|
||||
if not arg.can_be_inferred:
|
||||
builder.writeln('self.{0} = {0} # type: {1}',
|
||||
arg.name, arg.python_type_hint())
|
||||
continue
|
||||
|
||||
# Currently the only argument that can be
|
||||
# inferred are those called 'random_id'
|
||||
if arg.name == 'random_id':
|
||||
# Endianness doesn't really matter, and 'big' is shorter
|
||||
code = "int.from_bytes(os.urandom({}), 'big', signed=True)" \
|
||||
.format(8 if arg.type == 'long' else 4)
|
||||
|
||||
if arg.is_vector:
|
||||
# Currently for the case of "messages.forwardMessages"
|
||||
# Ensure we can infer the length from id:Vector<>
|
||||
if not next(
|
||||
a for a in args if a.name == 'id').is_vector:
|
||||
raise ValueError(
|
||||
'Cannot infer list of random ids for ', tlobject
|
||||
)
|
||||
code = '[{} for _ in range(len(id))]'.format(code)
|
||||
|
||||
builder.writeln(
|
||||
"self.random_id = random_id if random_id "
|
||||
"is not None else {}", code
|
||||
)
|
||||
else:
|
||||
raise ValueError('Cannot infer a value for ', arg)
|
||||
|
||||
builder.end_block()
|
||||
|
||||
# Write the resolve(self, client, utils) method
|
||||
if any(arg.type in AUTO_CASTS for arg in args):
|
||||
builder.writeln('def resolve(self, client, utils):')
|
||||
for arg in args:
|
||||
ac = AUTO_CASTS.get(arg.type, None)
|
||||
if ac:
|
||||
TLGenerator._write_self_assign(builder, arg, ac)
|
||||
builder.end_block()
|
||||
|
||||
# Write the to_dict(self) method
|
||||
builder.writeln('def to_dict(self, recursive=True):')
|
||||
if args:
|
||||
builder.writeln('return {')
|
||||
else:
|
||||
builder.write('return {')
|
||||
builder.writeln('def to_dict(self):')
|
||||
builder.writeln('return {')
|
||||
builder.current_indent += 1
|
||||
|
||||
base_types = ('string', 'bytes', 'int', 'long', 'int128',
|
||||
'int256', 'double', 'Bool', 'true', 'date')
|
||||
|
||||
builder.write("'_': '{}'", tlobject.class_name())
|
||||
for arg in args:
|
||||
builder.write("'{}': ".format(arg.name))
|
||||
builder.writeln(',')
|
||||
builder.write("'{}': ", arg.name)
|
||||
if arg.type in base_types:
|
||||
if arg.is_vector:
|
||||
builder.write('[] if self.{0} is None else self.{0}[:]'
|
||||
.format(arg.name))
|
||||
builder.write('[] if self.{0} is None else self.{0}[:]',
|
||||
arg.name)
|
||||
else:
|
||||
builder.write('self.{}'.format(arg.name))
|
||||
builder.write('self.{}', arg.name)
|
||||
else:
|
||||
if arg.is_vector:
|
||||
builder.write(
|
||||
'([] if self.{0} is None else [None'
|
||||
' if x is None else x.to_dict() for x in self.{0}]'
|
||||
') if recursive else self.{0}'.format(arg.name)
|
||||
'[] if self.{0} is None else [None '
|
||||
'if x is None else x.to_dict() for x in self.{0}]',
|
||||
arg.name
|
||||
)
|
||||
else:
|
||||
builder.write(
|
||||
'(None if self.{0} is None else self.{0}.to_dict())'
|
||||
' if recursive else self.{0}'.format(arg.name)
|
||||
'None if self.{0} is None else self.{0}.to_dict()',
|
||||
arg.name
|
||||
)
|
||||
builder.writeln(',')
|
||||
|
||||
builder.writeln()
|
||||
builder.current_indent -= 1
|
||||
builder.writeln("}")
|
||||
|
||||
|
@ -311,21 +405,22 @@ class TLGenerator:
|
|||
|
||||
for ra in repeated_args.values():
|
||||
if len(ra) > 1:
|
||||
cnd1 = ('self.{}'.format(a.name) for a in ra)
|
||||
cnd2 = ('not self.{}'.format(a.name) for a in ra)
|
||||
cnd1 = ('(self.{0} or self.{0} is not None)'
|
||||
.format(a.name) for a in ra)
|
||||
cnd2 = ('(self.{0} is None or self.{0} is False)'
|
||||
.format(a.name) for a in ra)
|
||||
builder.writeln(
|
||||
"assert ({}) or ({}), '{} parameters must all "
|
||||
"be False-y (like None) or all me True-y'".format(
|
||||
' and '.join(cnd1), ' and '.join(cnd2),
|
||||
', '.join(a.name for a in ra)
|
||||
)
|
||||
"be False-y (like None) or all me True-y'",
|
||||
' and '.join(cnd1), ' and '.join(cnd2),
|
||||
', '.join(a.name for a in ra)
|
||||
)
|
||||
|
||||
builder.writeln("return b''.join((")
|
||||
builder.current_indent += 1
|
||||
|
||||
# First constructor code, we already know its bytes
|
||||
builder.writeln('{},'.format(repr(struct.pack('<I', tlobject.id))))
|
||||
builder.writeln('{},', repr(struct.pack('<I', tlobject.id)))
|
||||
|
||||
for arg in tlobject.args:
|
||||
if TLGenerator.write_to_bytes(builder, arg, tlobject.args):
|
||||
|
@ -343,84 +438,51 @@ class TLGenerator:
|
|||
builder, arg, tlobject.args, name='_' + arg.name
|
||||
)
|
||||
|
||||
builder.writeln('return {}({})'.format(
|
||||
tlobject.class_name(), ', '.join(
|
||||
builder.writeln(
|
||||
'return {}({})',
|
||||
tlobject.class_name(),
|
||||
', '.join(
|
||||
'{0}=_{0}'.format(a.name) for a in tlobject.sorted_args()
|
||||
if not a.flag_indicator and not a.generic_definition
|
||||
)
|
||||
))
|
||||
builder.end_block()
|
||||
)
|
||||
|
||||
# Only requests can have a different response that's not their
|
||||
# serialized body, that is, we'll be setting their .result.
|
||||
if tlobject.is_function:
|
||||
#
|
||||
# The default behaviour is reading a TLObject too, so no need
|
||||
# to override it unless necessary.
|
||||
if tlobject.is_function and not TLGenerator._is_boxed(tlobject.result):
|
||||
builder.end_block()
|
||||
builder.writeln('def on_response(self, reader):')
|
||||
TLGenerator.write_request_result_code(builder, tlobject)
|
||||
builder.end_block()
|
||||
|
||||
# Write the __str__(self) and stringify(self) functions
|
||||
builder.writeln('def __str__(self):')
|
||||
builder.writeln('return TLObject.pretty_format(self)')
|
||||
builder.end_block()
|
||||
|
||||
builder.writeln('def stringify(self):')
|
||||
builder.writeln('return TLObject.pretty_format(self, indent=0)')
|
||||
# builder.end_block() # No need to end the last block
|
||||
|
||||
@staticmethod
|
||||
def _write_self_assigns(builder, tlobject, arg, args):
|
||||
if arg.can_be_inferred:
|
||||
# Currently the only argument that can be
|
||||
# inferred are those called 'random_id'
|
||||
if arg.name == 'random_id':
|
||||
# Endianness doesn't really matter, and 'big' is shorter
|
||||
code = "int.from_bytes(os.urandom({}), 'big', signed=True)"\
|
||||
.format(8 if arg.type == 'long' else 4)
|
||||
|
||||
if arg.is_vector:
|
||||
# Currently for the case of "messages.forwardMessages"
|
||||
# Ensure we can infer the length from id:Vector<>
|
||||
if not next(a for a in args if a.name == 'id').is_vector:
|
||||
raise ValueError(
|
||||
'Cannot infer list of random ids for ', tlobject
|
||||
)
|
||||
code = '[{} for _ in range(len(id))]'.format(code)
|
||||
|
||||
builder.writeln(
|
||||
"self.random_id = random_id if random_id "
|
||||
"is not None else {}".format(code)
|
||||
)
|
||||
else:
|
||||
raise ValueError('Cannot infer a value for ', arg)
|
||||
|
||||
# Well-known cases, auto-cast it to the right type
|
||||
elif arg.type == 'InputPeer' and tlobject.is_function:
|
||||
TLGenerator.write_get_input(builder, arg, 'get_input_peer')
|
||||
elif arg.type == 'InputChannel' and tlobject.is_function:
|
||||
TLGenerator.write_get_input(builder, arg, 'get_input_channel')
|
||||
elif arg.type == 'InputUser' and tlobject.is_function:
|
||||
TLGenerator.write_get_input(builder, arg, 'get_input_user')
|
||||
elif arg.type == 'InputMedia' and tlobject.is_function:
|
||||
TLGenerator.write_get_input(builder, arg, 'get_input_media')
|
||||
elif arg.type == 'InputPhoto' and tlobject.is_function:
|
||||
TLGenerator.write_get_input(builder, arg, 'get_input_photo')
|
||||
|
||||
else:
|
||||
builder.writeln('self.{0} = {0}'.format(arg.name))
|
||||
def _is_boxed(type_):
|
||||
# https://core.telegram.org/mtproto/serialize#boxed-and-bare-types
|
||||
# TL;DR; boxed types start with uppercase always, so we can use
|
||||
# this to check whether everything in it is boxed or not.
|
||||
#
|
||||
# The API always returns a boxed type, but it may inside a Vector<>
|
||||
# or a namespace, and the Vector may have a not-boxed type. For this
|
||||
# reason we find whatever index, '<' or '.'. If neither are present
|
||||
# we will get -1, and the 0th char is always upper case thus works.
|
||||
# For Vector types and namespaces, it will check in the right place.
|
||||
check_after = max(type_.find('<'), type_.find('.'))
|
||||
return type_[check_after + 1].isupper()
|
||||
|
||||
@staticmethod
|
||||
def write_get_input(builder, arg, get_input_code):
|
||||
"""Returns "True" if the get_input_* code was written when assigning
|
||||
a parameter upon creating the request. Returns False otherwise
|
||||
"""
|
||||
def _write_self_assign(builder, arg, get_input_code):
|
||||
"""Writes self.arg = input.format(self.arg), considering vectors."""
|
||||
if arg.is_vector:
|
||||
builder.write('self.{0} = [{1}(_x) for _x in {0}]'
|
||||
.format(arg.name, get_input_code))
|
||||
builder.write('self.{0} = [{1} for _x in self.{0}]',
|
||||
arg.name, get_input_code.format('_x'))
|
||||
else:
|
||||
builder.write('self.{0} = {1}({0})'
|
||||
.format(arg.name, get_input_code))
|
||||
builder.write('self.{} = {}',
|
||||
arg.name, get_input_code.format('self.' + arg.name))
|
||||
|
||||
builder.writeln(
|
||||
' if {} else None'.format(arg.name) if arg.is_flag else ''
|
||||
' if self.{} else None'.format(arg.name) if arg.is_flag else ''
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@ -465,17 +527,17 @@ class TLGenerator:
|
|||
# so we need an extra join here. Note that empty vector flags
|
||||
# should NOT be sent either!
|
||||
builder.write("b'' if {0} is None or {0} is False "
|
||||
"else b''.join((".format(name))
|
||||
"else b''.join((", name)
|
||||
else:
|
||||
builder.write("b'' if {0} is None or {0} is False "
|
||||
"else (".format(name))
|
||||
"else (", name)
|
||||
|
||||
if arg.is_vector:
|
||||
if arg.use_vector_id:
|
||||
# vector code, unsigned 0x1cb5c415 as little endian
|
||||
builder.write(r"b'\x15\xc4\xb5\x1c',")
|
||||
|
||||
builder.write("struct.pack('<i', len({})),".format(name))
|
||||
builder.write("struct.pack('<i', len({})),", name)
|
||||
|
||||
# Cannot unpack the values for the outer tuple through *[(
|
||||
# since that's a Python >3.5 feature, so add another join.
|
||||
|
@ -489,7 +551,7 @@ class TLGenerator:
|
|||
arg.is_vector = True
|
||||
arg.is_flag = old_flag
|
||||
|
||||
builder.write(' for x in {})'.format(name))
|
||||
builder.write(' for x in {})', name)
|
||||
|
||||
elif arg.flag_indicator:
|
||||
# Calculate the flags with those items which are not None
|
||||
|
@ -508,45 +570,39 @@ class TLGenerator:
|
|||
|
||||
elif 'int' == arg.type:
|
||||
# struct.pack is around 4 times faster than int.to_bytes
|
||||
builder.write("struct.pack('<i', {})".format(name))
|
||||
builder.write("struct.pack('<i', {})", name)
|
||||
|
||||
elif 'long' == arg.type:
|
||||
builder.write("struct.pack('<q', {})".format(name))
|
||||
builder.write("struct.pack('<q', {})", name)
|
||||
|
||||
elif 'int128' == arg.type:
|
||||
builder.write("{}.to_bytes(16, 'little', signed=True)".format(name))
|
||||
builder.write("{}.to_bytes(16, 'little', signed=True)", name)
|
||||
|
||||
elif 'int256' == arg.type:
|
||||
builder.write("{}.to_bytes(32, 'little', signed=True)".format(name))
|
||||
builder.write("{}.to_bytes(32, 'little', signed=True)", name)
|
||||
|
||||
elif 'double' == arg.type:
|
||||
builder.write("struct.pack('<d', {})".format(name))
|
||||
builder.write("struct.pack('<d', {})", name)
|
||||
|
||||
elif 'string' == arg.type:
|
||||
builder.write('TLObject.serialize_bytes({})'.format(name))
|
||||
builder.write('TLObject.serialize_bytes({})', name)
|
||||
|
||||
elif 'Bool' == arg.type:
|
||||
# 0x997275b5 if boolean else 0xbc799737
|
||||
builder.write(
|
||||
r"b'\xb5ur\x99' if {} else b'7\x97y\xbc'".format(name)
|
||||
)
|
||||
builder.write(r"b'\xb5ur\x99' if {} else b'7\x97y\xbc'", name)
|
||||
|
||||
elif 'true' == arg.type:
|
||||
pass # These are actually NOT written! Only used for flags
|
||||
|
||||
elif 'bytes' == arg.type:
|
||||
builder.write('TLObject.serialize_bytes({})'.format(name))
|
||||
builder.write('TLObject.serialize_bytes({})', name)
|
||||
|
||||
elif 'date' == arg.type: # Custom format
|
||||
# 0 if datetime is None else int(datetime.timestamp())
|
||||
builder.write(
|
||||
r"b'\0\0\0\0' if {0} is None else "
|
||||
r"struct.pack('<I', int({0}.timestamp()))".format(name)
|
||||
)
|
||||
builder.write('TLObject.serialize_datetime({})', name)
|
||||
|
||||
else:
|
||||
# Else it may be a custom type
|
||||
builder.write('bytes({})'.format(name))
|
||||
builder.write('bytes({})', name)
|
||||
|
||||
if arg.is_flag:
|
||||
builder.write(')')
|
||||
|
@ -579,15 +635,12 @@ class TLGenerator:
|
|||
# Treat 'true' flags as a special case, since they're true if
|
||||
# they're set, and nothing else needs to actually be read.
|
||||
if 'true' == arg.type:
|
||||
builder.writeln(
|
||||
'{} = bool(flags & {})'.format(name, 1 << arg.flag_index)
|
||||
)
|
||||
builder.writeln('{} = bool(flags & {})',
|
||||
name, 1 << arg.flag_index)
|
||||
return
|
||||
|
||||
was_flag = True
|
||||
builder.writeln('if flags & {}:'.format(
|
||||
1 << arg.flag_index
|
||||
))
|
||||
builder.writeln('if flags & {}:', 1 << arg.flag_index)
|
||||
# Temporary disable .is_flag not to enter this if
|
||||
# again when calling the method recursively
|
||||
arg.is_flag = False
|
||||
|
@ -597,12 +650,12 @@ class TLGenerator:
|
|||
# We have to read the vector's constructor ID
|
||||
builder.writeln("reader.read_int()")
|
||||
|
||||
builder.writeln('{} = []'.format(name))
|
||||
builder.writeln('{} = []', name)
|
||||
builder.writeln('for _ in range(reader.read_int()):')
|
||||
# Temporary disable .is_vector, not to enter this if again
|
||||
arg.is_vector = False
|
||||
TLGenerator.write_read_code(builder, arg, args, name='_x')
|
||||
builder.writeln('{}.append(_x)'.format(name))
|
||||
builder.writeln('{}.append(_x)', name)
|
||||
arg.is_vector = True
|
||||
|
||||
elif arg.flag_indicator:
|
||||
|
@ -611,44 +664,40 @@ class TLGenerator:
|
|||
builder.writeln()
|
||||
|
||||
elif 'int' == arg.type:
|
||||
builder.writeln('{} = reader.read_int()'.format(name))
|
||||
builder.writeln('{} = reader.read_int()', name)
|
||||
|
||||
elif 'long' == arg.type:
|
||||
builder.writeln('{} = reader.read_long()'.format(name))
|
||||
builder.writeln('{} = reader.read_long()', name)
|
||||
|
||||
elif 'int128' == arg.type:
|
||||
builder.writeln(
|
||||
'{} = reader.read_large_int(bits=128)'.format(name)
|
||||
)
|
||||
builder.writeln('{} = reader.read_large_int(bits=128)', name)
|
||||
|
||||
elif 'int256' == arg.type:
|
||||
builder.writeln(
|
||||
'{} = reader.read_large_int(bits=256)'.format(name)
|
||||
)
|
||||
builder.writeln('{} = reader.read_large_int(bits=256)', name)
|
||||
|
||||
elif 'double' == arg.type:
|
||||
builder.writeln('{} = reader.read_double()'.format(name))
|
||||
builder.writeln('{} = reader.read_double()', name)
|
||||
|
||||
elif 'string' == arg.type:
|
||||
builder.writeln('{} = reader.tgread_string()'.format(name))
|
||||
builder.writeln('{} = reader.tgread_string()', name)
|
||||
|
||||
elif 'Bool' == arg.type:
|
||||
builder.writeln('{} = reader.tgread_bool()'.format(name))
|
||||
builder.writeln('{} = reader.tgread_bool()', name)
|
||||
|
||||
elif 'true' == arg.type:
|
||||
# Arbitrary not-None value, don't actually read "true" flags
|
||||
builder.writeln('{} = True'.format(name))
|
||||
builder.writeln('{} = True', name)
|
||||
|
||||
elif 'bytes' == arg.type:
|
||||
builder.writeln('{} = reader.tgread_bytes()'.format(name))
|
||||
builder.writeln('{} = reader.tgread_bytes()', name)
|
||||
|
||||
elif 'date' == arg.type: # Custom format
|
||||
builder.writeln('{} = reader.tgread_date()'.format(name))
|
||||
builder.writeln('{} = reader.tgread_date()', name)
|
||||
|
||||
else:
|
||||
# Else it may be a custom type
|
||||
if not arg.skip_constructor_id:
|
||||
builder.writeln('{} = reader.tgread_object()'.format(name))
|
||||
builder.writeln('{} = reader.tgread_object()', name)
|
||||
else:
|
||||
# Import the correct type inline to avoid cyclic imports.
|
||||
# There may be better solutions so that we can just access
|
||||
|
@ -665,10 +714,9 @@ class TLGenerator:
|
|||
# file with the same namespace, but since it does no harm
|
||||
# and we don't have information about such thing in the
|
||||
# method we just ignore that case.
|
||||
builder.writeln('from {} import {}'.format(ns, class_name))
|
||||
builder.writeln('{} = {}.from_reader(reader)'.format(
|
||||
name, class_name
|
||||
))
|
||||
builder.writeln('from {} import {}', ns, class_name)
|
||||
builder.writeln('{} = {}.from_reader(reader)',
|
||||
name, class_name)
|
||||
|
||||
# End vector and flag blocks if required (if we opened them before)
|
||||
if arg.is_vector:
|
||||
|
@ -677,7 +725,7 @@ class TLGenerator:
|
|||
if was_flag:
|
||||
builder.current_indent -= 1
|
||||
builder.writeln('else:')
|
||||
builder.writeln('{} = None'.format(name))
|
||||
builder.writeln('{} = None', name)
|
||||
builder.current_indent -= 1
|
||||
# Restore .is_flag
|
||||
arg.is_flag = True
|
||||
|
@ -697,13 +745,13 @@ class TLGenerator:
|
|||
# not parsed as arguments are and it's a bit harder to tell which
|
||||
# is which.
|
||||
if tlobject.result == 'Vector<int>':
|
||||
builder.writeln('reader.read_int() # Vector id')
|
||||
builder.writeln('reader.read_int() # Vector ID')
|
||||
builder.writeln('count = reader.read_int()')
|
||||
builder.writeln(
|
||||
'self.result = [reader.read_int() for _ in range(count)]'
|
||||
)
|
||||
elif tlobject.result == 'Vector<long>':
|
||||
builder.writeln('reader.read_int() # Vector id')
|
||||
builder.writeln('reader.read_int() # Vector ID')
|
||||
builder.writeln('count = reader.read_long()')
|
||||
builder.writeln(
|
||||
'self.result = [reader.read_long() for _ in range(count)]'
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import unittest
|
||||
|
||||
|
||||
class ParserTests(unittest.TestCase):
|
||||
"""There are no tests yet"""
|
|
@ -3,8 +3,7 @@ from hashlib import sha1
|
|||
|
||||
import telethon.helpers as utils
|
||||
from telethon.crypto import AES, Factorization
|
||||
from telethon.crypto import rsa
|
||||
from Crypto.PublicKey import RSA as PyCryptoRSA
|
||||
# from crypto.PublicKey import RSA as PyCryptoRSA
|
||||
|
||||
|
||||
class CryptoTests(unittest.TestCase):
|
||||
|
@ -22,37 +21,39 @@ class CryptoTests(unittest.TestCase):
|
|||
self.cipher_text_padded = b"W\xd1\xed'\x01\xa6c\xc3\xcb\xef\xaa\xe5\x1d\x1a" \
|
||||
b"[\x1b\xdf\xcdI\x1f>Z\n\t\xb9\xd2=\xbaF\xd1\x8e'"
|
||||
|
||||
@staticmethod
|
||||
def test_sha1():
|
||||
def test_sha1(self):
|
||||
string = 'Example string'
|
||||
|
||||
hash_sum = sha1(string.encode('utf-8')).digest()
|
||||
expected = b'\nT\x92|\x8d\x06:)\x99\x04\x8e\xf8j?\xc4\x8e\xd3}m9'
|
||||
|
||||
assert hash_sum == expected, 'Invalid sha1 hash_sum representation (should be {}, but is {})'\
|
||||
.format(expected, hash_sum)
|
||||
self.assertEqual(hash_sum, expected,
|
||||
msg='Invalid sha1 hash_sum representation (should be {}, but is {})'
|
||||
.format(expected, hash_sum))
|
||||
|
||||
@unittest.skip("test_aes_encrypt needs fix")
|
||||
def test_aes_encrypt(self):
|
||||
value = AES.encrypt_ige(self.plain_text, self.key, self.iv)
|
||||
take = 16 # Don't take all the bytes, since latest involve are random padding
|
||||
assert value[:take] == self.cipher_text[:take],\
|
||||
('Ciphered text ("{}") does not equal expected ("{}")'
|
||||
.format(value[:take], self.cipher_text[:take]))
|
||||
self.assertEqual(value[:take], self.cipher_text[:take],
|
||||
msg='Ciphered text ("{}") does not equal expected ("{}")'
|
||||
.format(value[:take], self.cipher_text[:take]))
|
||||
|
||||
value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv)
|
||||
assert value == self.cipher_text_padded, (
|
||||
'Ciphered text ("{}") does not equal expected ("{}")'
|
||||
.format(value, self.cipher_text_padded))
|
||||
self.assertEqual(value, self.cipher_text_padded,
|
||||
msg='Ciphered text ("{}") does not equal expected ("{}")'
|
||||
.format(value, self.cipher_text_padded))
|
||||
|
||||
def test_aes_decrypt(self):
|
||||
# The ciphered text must always be padded
|
||||
value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv)
|
||||
assert value == self.plain_text_padded, (
|
||||
'Decrypted text ("{}") does not equal expected ("{}")'
|
||||
.format(value, self.plain_text_padded))
|
||||
self.assertEqual(value, self.plain_text_padded,
|
||||
msg='Decrypted text ("{}") does not equal expected ("{}")'
|
||||
.format(value, self.plain_text_padded))
|
||||
|
||||
@staticmethod
|
||||
def test_calc_key():
|
||||
@unittest.skip("test_calc_key needs fix")
|
||||
def test_calc_key(self):
|
||||
# TODO Upgrade test for MtProto 2.0
|
||||
shared_key = b'\xbc\xd2m\xb7\xcav\xf4][\x88\x83\' \xf3\x11\x8as\xd04\x941\xae' \
|
||||
b'*O\x03\x86\x9a/H#\x1a\x8c\xb5j\xe9$\xe0IvCm^\xe70\x1a5C\t\x16' \
|
||||
b'\x03\xd2\x9d\xa9\x89\xd6\xce\x08P\x0fdr\xa0\xb3\xeb\xfecv\x1a' \
|
||||
|
@ -77,10 +78,12 @@ class CryptoTests(unittest.TestCase):
|
|||
b'\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1' \
|
||||
b'\xa7\xa0\xf7\x0f'
|
||||
|
||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
|
||||
expected_key, key)
|
||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
|
||||
expected_iv, iv)
|
||||
self.assertEqual(key, expected_key,
|
||||
msg='Invalid key (expected ("{}"), got ("{}"))'
|
||||
.format(expected_key, key))
|
||||
self.assertEqual(iv, expected_iv,
|
||||
msg='Invalid IV (expected ("{}"), got ("{}"))'
|
||||
.format(expected_iv, iv))
|
||||
|
||||
# Calculate key being the server
|
||||
msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]'
|
||||
|
@ -93,20 +96,14 @@ class CryptoTests(unittest.TestCase):
|
|||
expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd' \
|
||||
b'\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc'
|
||||
|
||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
|
||||
expected_key, key)
|
||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
|
||||
expected_iv, iv)
|
||||
self.assertEqual(key, expected_key,
|
||||
msg='Invalid key (expected ("{}"), got ("{}"))'
|
||||
.format(expected_key, key))
|
||||
self.assertEqual(iv, expected_iv,
|
||||
msg='Invalid IV (expected ("{}"), got ("{}"))'
|
||||
.format(expected_iv, iv))
|
||||
|
||||
@staticmethod
|
||||
def test_calc_msg_key():
|
||||
value = utils.calc_msg_key(b'Some random message')
|
||||
expected = b'\xdfAa\xfc\x10\xab\x89\xd2\xfe\x19C\xf1\xdd~\xbf\x81'
|
||||
assert value == expected, 'Value ("{}") does not equal expected ("{}")'.format(
|
||||
value, expected)
|
||||
|
||||
@staticmethod
|
||||
def test_generate_key_data_from_nonce():
|
||||
def test_generate_key_data_from_nonce(self):
|
||||
server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little')
|
||||
new_nonce = int.from_bytes(b'The new, calculated 32-bit nonce', byteorder='little')
|
||||
|
||||
|
@ -114,30 +111,33 @@ class CryptoTests(unittest.TestCase):
|
|||
expected_key = b'/\xaa\x7f\xa1\xfcs\xef\xa0\x99zh\x03M\xa4\x8e\xb4\xab\x0eE]b\x95|\xfe\xc0\xf8\x1f\xd4\xa0\xd4\xec\x91'
|
||||
expected_iv = b'\xf7\xae\xe3\xc8+=\xc2\xb8\xd1\xe1\x1b\x0e\x10\x07\x9fn\x9e\xdc\x960\x05\xf9\xea\xee\x8b\xa1h The '
|
||||
|
||||
assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(
|
||||
key, expected_key)
|
||||
assert iv == expected_iv, 'IV ("{}") does not equal expected ("{}")'.format(
|
||||
iv, expected_iv)
|
||||
self.assertEqual(key, expected_key,
|
||||
msg='Key ("{}") does not equal expected ("{}")'
|
||||
.format(key, expected_key))
|
||||
self.assertEqual(iv, expected_iv,
|
||||
msg='IV ("{}") does not equal expected ("{}")'
|
||||
.format(iv, expected_iv))
|
||||
|
||||
@staticmethod
|
||||
def test_fingerprint_from_key():
|
||||
assert rsa._compute_fingerprint(PyCryptoRSA.importKey(
|
||||
'-----BEGIN RSA PUBLIC KEY-----\n'
|
||||
'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n'
|
||||
'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n'
|
||||
'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n'
|
||||
'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n'
|
||||
'8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n'
|
||||
'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n'
|
||||
'-----END RSA PUBLIC KEY-----'
|
||||
)) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated'
|
||||
# test_fringerprint_from_key can't be skipped due to ImportError
|
||||
# def test_fingerprint_from_key(self):
|
||||
# assert rsa._compute_fingerprint(PyCryptoRSA.importKey(
|
||||
# '-----BEGIN RSA PUBLIC KEY-----\n'
|
||||
# 'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n'
|
||||
# 'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n'
|
||||
# 'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n'
|
||||
# 'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n'
|
||||
# '8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n'
|
||||
# 'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n'
|
||||
# '-----END RSA PUBLIC KEY-----'
|
||||
# )) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated'
|
||||
|
||||
@staticmethod
|
||||
def test_factorize():
|
||||
def test_factorize(self):
|
||||
pq = 3118979781119966969
|
||||
p, q = Factorization.factorize(pq)
|
||||
if p > q:
|
||||
p, q = q, p
|
||||
|
||||
assert p == 1719614201, 'Factorized pair did not yield the correct result'
|
||||
assert q == 1813767169, 'Factorized pair did not yield the correct result'
|
||||
self.assertEqual(p, 1719614201,
|
||||
msg='Factorized pair did not yield the correct result')
|
||||
self.assertEqual(q, 1813767169,
|
||||
msg='Factorized pair did not yield the correct result')
|
|
@ -10,16 +10,17 @@ from telethon import TelegramClient
|
|||
api_id = None
|
||||
api_hash = None
|
||||
|
||||
if not api_id or not api_hash:
|
||||
raise ValueError('Please fill in both your api_id and api_hash.')
|
||||
|
||||
|
||||
class HigherLevelTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def test_cdn_download():
|
||||
def setUp(self):
|
||||
if not api_id or not api_hash:
|
||||
raise ValueError('Please fill in both your api_id and api_hash.')
|
||||
|
||||
@unittest.skip("you can't seriously trash random mobile numbers like that :)")
|
||||
def test_cdn_download(self):
|
||||
client = TelegramClient(None, api_id, api_hash)
|
||||
client.session.server_address = '149.154.167.40'
|
||||
assert client.connect()
|
||||
client.session.set_dc(0, '149.154.167.40', 80)
|
||||
self.assertTrue(client.connect())
|
||||
|
||||
try:
|
||||
phone = '+999662' + str(randint(0, 9999)).zfill(4)
|
||||
|
@ -37,11 +38,11 @@ class HigherLevelTests(unittest.TestCase):
|
|||
|
||||
out = BytesIO()
|
||||
client.download_media(msg, out)
|
||||
assert sha256(data).digest() == sha256(out.getvalue()).digest()
|
||||
self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest())
|
||||
|
||||
out = BytesIO()
|
||||
client.download_media(msg, out) # Won't redirect
|
||||
assert sha256(data).digest() == sha256(out.getvalue()).digest()
|
||||
self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest())
|
||||
|
||||
client.log_out()
|
||||
finally:
|
|
@ -23,8 +23,9 @@ def run_server_echo_thread(port):
|
|||
|
||||
|
||||
class NetworkTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def test_tcp_client():
|
||||
|
||||
@unittest.skip("test_tcp_client needs fix")
|
||||
def test_tcp_client(self):
|
||||
port = random.randint(50000, 60000) # Arbitrary non-privileged port
|
||||
run_server_echo_thread(port)
|
||||
|
||||
|
@ -32,12 +33,12 @@ class NetworkTests(unittest.TestCase):
|
|||
client = TcpClient()
|
||||
client.connect('localhost', port)
|
||||
client.write(msg)
|
||||
assert msg == client.read(
|
||||
15), 'Read message does not equal sent message'
|
||||
self.assertEqual(msg, client.read(15),
|
||||
msg='Read message does not equal sent message')
|
||||
client.close()
|
||||
|
||||
@staticmethod
|
||||
def test_authenticator():
|
||||
@unittest.skip("Some parameters changed, so IP doesn't go there anymore.")
|
||||
def test_authenticator(self):
|
||||
transport = Connection('149.154.167.91', 443)
|
||||
authenticator.do_authentication(transport)
|
||||
self.assertTrue(authenticator.do_authentication(transport))
|
||||
transport.close()
|
8
telethon_tests/test_parser.py
Normal file
8
telethon_tests/test_parser.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
import unittest
|
||||
|
||||
|
||||
class ParserTests(unittest.TestCase):
|
||||
"""There are no tests yet"""
|
||||
@unittest.skip("there should be parser tests")
|
||||
def test_parser(self):
|
||||
self.assertTrue(True)
|
8
telethon_tests/test_tl.py
Normal file
8
telethon_tests/test_tl.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
import unittest
|
||||
|
||||
|
||||
class TLTests(unittest.TestCase):
|
||||
"""There are no tests yet"""
|
||||
@unittest.skip("there should be TL tests")
|
||||
def test_tl(self):
|
||||
self.assertTrue(True)
|
|
@ -5,8 +5,7 @@ from telethon.extensions import BinaryReader
|
|||
|
||||
|
||||
class UtilsTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def test_binary_writer_reader():
|
||||
def test_binary_writer_reader(self):
|
||||
# Test that we can read properly
|
||||
data = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
|
||||
b'\x88A\x00\x00\x00\x00\x00\x009@\x1a\x1b\x1c\x1d\x1e\x1f ' \
|
||||
|
@ -15,29 +14,35 @@ class UtilsTests(unittest.TestCase):
|
|||
|
||||
with BinaryReader(data) as reader:
|
||||
value = reader.read_byte()
|
||||
assert value == 1, 'Example byte should be 1 but is {}'.format(value)
|
||||
self.assertEqual(value, 1,
|
||||
msg='Example byte should be 1 but is {}'.format(value))
|
||||
|
||||
value = reader.read_int()
|
||||
assert value == 5, 'Example integer should be 5 but is {}'.format(value)
|
||||
self.assertEqual(value, 5,
|
||||
msg='Example integer should be 5 but is {}'.format(value))
|
||||
|
||||
value = reader.read_long()
|
||||
assert value == 13, 'Example long integer should be 13 but is {}'.format(value)
|
||||
self.assertEqual(value, 13,
|
||||
msg='Example long integer should be 13 but is {}'.format(value))
|
||||
|
||||
value = reader.read_float()
|
||||
assert value == 17.0, 'Example float should be 17.0 but is {}'.format(value)
|
||||
self.assertEqual(value, 17.0,
|
||||
msg='Example float should be 17.0 but is {}'.format(value))
|
||||
|
||||
value = reader.read_double()
|
||||
assert value == 25.0, 'Example double should be 25.0 but is {}'.format(value)
|
||||
self.assertEqual(value, 25.0,
|
||||
msg='Example double should be 25.0 but is {}'.format(value))
|
||||
|
||||
value = reader.read(7)
|
||||
assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \
|
||||
.format(bytes([26, 27, 28, 29, 30, 31, 32]), value)
|
||||
self.assertEqual(value, bytes([26, 27, 28, 29, 30, 31, 32]),
|
||||
msg='Example bytes should be {} but is {}'
|
||||
.format(bytes([26, 27, 28, 29, 30, 31, 32]), value))
|
||||
|
||||
value = reader.read_large_int(128, signed=False)
|
||||
assert value == 2**127, 'Example large integer should be {} but is {}'.format(2**127, value)
|
||||
self.assertEqual(value, 2**127,
|
||||
msg='Example large integer should be {} but is {}'.format(2**127, value))
|
||||
|
||||
@staticmethod
|
||||
def test_binary_tgwriter_tgreader():
|
||||
def test_binary_tgwriter_tgreader(self):
|
||||
small_data = os.urandom(33)
|
||||
small_data_padded = os.urandom(19) # +1 byte for length = 20 (%4 = 0)
|
||||
|
||||
|
@ -53,9 +58,9 @@ class UtilsTests(unittest.TestCase):
|
|||
# And then try reading it without errors (it should be unharmed!)
|
||||
for datum in data:
|
||||
value = reader.tgread_bytes()
|
||||
assert value == datum, 'Example bytes should be {} but is {}'.format(
|
||||
datum, value)
|
||||
self.assertEqual(value, datum,
|
||||
msg='Example bytes should be {} but is {}'.format(datum, value))
|
||||
|
||||
value = reader.tgread_string()
|
||||
assert value == string, 'Example string should be {} but is {}'.format(
|
||||
string, value)
|
||||
self.assertEqual(value, string,
|
||||
msg='Example string should be {} but is {}'.format(string, value))
|
|
@ -1,5 +0,0 @@
|
|||
import unittest
|
||||
|
||||
|
||||
class TLTests(unittest.TestCase):
|
||||
"""There are no tests yet"""
|
Loading…
Reference in New Issue
Block a user