merge upstream

This commit is contained in:
Tanuj 2018-04-03 09:55:16 +01:00
commit 05e7e2aa05
100 changed files with 9670 additions and 3682 deletions

View File

@ -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.

View File

@ -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):

View File

@ -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')

View File

@ -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&lt;InputUser&gt; = Vector&lt;User&gt;</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>

View File

@ -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
View 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;
}

View File

@ -0,0 +1,3 @@
cryptg
pysocks
hachoir3

View File

@ -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'),
]

View 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

View 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

View 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.

View 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!

View 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

View File

@ -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 youve 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 cant 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 dont 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 dont want to add yourself, maybe because youre 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 dont need to join but rather check whether its 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 youre 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
youll 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 dont 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

View File

@ -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

View File

@ -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

View File

@ -1,117 +0,0 @@
.. _accessing-the-full-api:
==========================
Accessing the Full API
==========================
The ``TelegramClient`` doesnt offer a method for every single request
the Telegram API supports. However, its very simple to ``.invoke()``
any request. Whenever you need something, dont 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 thats the reason for this distinction.
Say ``client.send_message()`` didnt 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 wont be aware of this class! Now we have:
.. code-block:: python
from telethon.tl.functions.messages import SendMessageRequest
If youre 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 youre 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 doesnt 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

View File

@ -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

View 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.

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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 youve
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 youre not going to work without updates, or dont need to cache the
``access_hash`` associated with the entities ID, you can disable this
by setting ``client.session.save_entities = False``.
If you dont 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.

View 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

View File

@ -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

File diff suppressed because it is too large Load Diff

View 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

View 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``!

View 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.

View 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.

View File

@ -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>`__.

View 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')

View File

@ -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!

View File

@ -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.

View File

@ -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
))
Its 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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -2,26 +2,28 @@
RPC Errors
==========
RPC stands for Remote Procedure Call, and when Telethon raises an
``RPCError``, its 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 Telegrams 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 dont 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 youre 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``.

View 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

View File

@ -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

View File

@ -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
----------------------------

View File

@ -1,3 +1,6 @@
.. _telethon-errors-package:
telethon\.errors package
========================

View File

@ -0,0 +1,9 @@
.. _telethon-events-package:
telethon\.events package
========================
.. automodule:: telethon.events
:members:
:undoc-members:
:show-inheritance:

View File

@ -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::

View File

@ -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:

View File

@ -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
View File

@ -0,0 +1,3 @@
pyaes
rsa
typing

View File

@ -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']
}
)

View File

@ -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)

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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))

View File

@ -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]

View File

@ -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

View File

@ -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):

View File

@ -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))

View File

@ -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]

View File

@ -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):

View File

@ -0,0 +1,3 @@
from .abstract import Session
from .memory import MemorySession
from .sqlite import SQLiteSession

View 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
View 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()

View File

@ -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

View File

@ -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

View File

@ -1,2 +1,3 @@
from .draft import Draft
from .dialog import Dialog
from .input_sized_file import InputSizedFile

View File

@ -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)

View File

@ -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)

View 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

View File

@ -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]

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
))

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)]'

View File

@ -1,5 +0,0 @@
import unittest
class ParserTests(unittest.TestCase):
"""There are no tests yet"""

View File

@ -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')

View File

@ -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:

View File

@ -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()

View 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)

View 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)

View File

@ -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))

View File

@ -1,5 +0,0 @@
import unittest
class TLTests(unittest.TestCase):
"""There are no tests yet"""